diff --git a/packages/widgets/src/notebook/component.tsx b/packages/widgets/src/notebook/component.tsx index e8dc77498..b8ef2191b 100644 --- a/packages/widgets/src/notebook/component.tsx +++ b/packages/widgets/src/notebook/component.tsx @@ -1,33 +1,38 @@ "use client"; import dynamic from "next/dynamic"; +import { useEffect, useState } from "react"; import { useScopedI18n } from "@homarr/translation/client"; import { WidgetMobileSummary } from "../common/mobile-summary"; import type { WidgetComponentProps } from "../definition"; +import { extractNotebookExcerpt } from "./mobile-summary"; const Notebook = dynamic(() => import("./notebook").then((module) => module.Notebook), { ssr: false, }); +interface NotebookMobileSummaryProps { + content: string; + label: string; +} + +const NotebookMobileSummary = ({ content, label }: NotebookMobileSummaryProps) => { + const [excerpt, setExcerpt] = useState(""); + + useEffect(() => { + setExcerpt(extractNotebookExcerpt(content)); + }, [content]); + + return ; +}; + export default function NotebookWidget(props: WidgetComponentProps<"notebook">) { const t = useScopedI18n("widget.notebook"); if (props.displayMode === "mobileSummary") { - const excerpt = props.options.content - .replace(/]*>[\s\S]*?<\/style>/gi, " ") - .replace(/]*>[\s\S]*?<\/script>/gi, " ") - .replace(/<[^>]+>/g, " ") - .replace(/&(?:nbsp|#160);/gi, " ") - .replace(/&(?:amp|#38);/gi, "&") - .replace(/&(?:lt|#60);/gi, "<") - .replace(/&(?:gt|#62);/gi, ">") - .replace(/&(?:quot|#34);/gi, '"') - .replace(/\s+/g, " ") - .trim(); - - return ; + return ; } return ; diff --git a/packages/widgets/src/notebook/mobile-summary.spec.ts b/packages/widgets/src/notebook/mobile-summary.spec.ts new file mode 100644 index 000000000..992867ba6 --- /dev/null +++ b/packages/widgets/src/notebook/mobile-summary.spec.ts @@ -0,0 +1,20 @@ +import { describe, expect, test } from "vitest"; + +import { extractNotebookExcerpt } from "./mobile-summary"; + +describe("extractNotebookExcerpt", () => { + test("returns decoded plain text from notebook HTML", () => { + expect(extractNotebookExcerpt("

Hello world

Fish & chips

")).toBe("Hello world Fish & chips"); + }); + + test("removes script and style content including whitespace before closing brackets", () => { + const content = + '

Visible

Still visible

'; + + expect(extractNotebookExcerpt(content)).toBe("Visible Still visible"); + }); + + test("returns an empty excerpt when there is no visible content", () => { + expect(extractNotebookExcerpt("")).toBe(""); + }); +}); diff --git a/packages/widgets/src/notebook/mobile-summary.ts b/packages/widgets/src/notebook/mobile-summary.ts new file mode 100644 index 000000000..dc935d3df --- /dev/null +++ b/packages/widgets/src/notebook/mobile-summary.ts @@ -0,0 +1,15 @@ +export const extractNotebookExcerpt = (content: string) => { + const parsedDocument = new DOMParser().parseFromString(content, "text/html"); + parsedDocument.querySelectorAll("script, style").forEach((element) => { + element.remove(); + }); + + const textWalker = parsedDocument.createTreeWalker(parsedDocument.body, NodeFilter.SHOW_TEXT); + const textParts: string[] = []; + + while (textWalker.nextNode()) { + textParts.push(textWalker.currentNode.textContent ?? ""); + } + + return textParts.join(" ").replace(/\s+/g, " ").trim(); +};