create-zudo-doc 3.2.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/dist/claude-md-gen.d.ts +8 -0
  2. package/dist/claude-md-gen.js +43 -26
  3. package/dist/compose.d.ts +14 -20
  4. package/dist/compose.js +15 -25
  5. package/dist/features/body-foot-util.d.ts +8 -4
  6. package/dist/features/body-foot-util.js +8 -4
  7. package/dist/features/claude-resources.d.ts +12 -0
  8. package/dist/features/claude-resources.js +13 -6
  9. package/dist/features/design-token-panel.d.ts +0 -12
  10. package/dist/features/design-token-panel.js +32 -93
  11. package/dist/features/doc-history.d.ts +18 -3
  12. package/dist/features/doc-history.js +45 -60
  13. package/dist/features/doc-tags.d.ts +9 -13
  14. package/dist/features/doc-tags.js +10 -26
  15. package/dist/features/dynamic-page-transition.d.ts +19 -30
  16. package/dist/features/dynamic-page-transition.js +21 -209
  17. package/dist/features/footer-taglist.d.ts +1 -1
  18. package/dist/features/footer-taglist.js +1 -1
  19. package/dist/features/footer.d.ts +3 -2
  20. package/dist/features/footer.js +3 -2
  21. package/dist/features/i18n.d.ts +13 -8
  22. package/dist/features/i18n.js +14 -9
  23. package/dist/features/image-enlarge.d.ts +7 -26
  24. package/dist/features/image-enlarge.js +7 -26
  25. package/dist/features/llms-txt.d.ts +3 -5
  26. package/dist/features/llms-txt.js +3 -5
  27. package/dist/features/search.d.ts +7 -3
  28. package/dist/features/search.js +7 -3
  29. package/dist/features/sidebar-resizer.d.ts +4 -8
  30. package/dist/features/sidebar-resizer.js +4 -8
  31. package/dist/features/sidebar-toggle.d.ts +7 -7
  32. package/dist/features/sidebar-toggle.js +7 -7
  33. package/dist/features/tag-governance.d.ts +14 -7
  34. package/dist/features/tag-governance.js +52 -8
  35. package/dist/features/tauri.d.ts +13 -10
  36. package/dist/features/tauri.js +16 -52
  37. package/dist/features/versioning.d.ts +12 -24
  38. package/dist/features/versioning.js +13 -39
  39. package/dist/index.js +2 -3
  40. package/dist/scaffold.d.ts +13 -6
  41. package/dist/scaffold.js +64 -45
  42. package/dist/utils.d.ts +10 -0
  43. package/dist/utils.js +14 -0
  44. package/dist/zfb-config-gen.d.ts +32 -20
  45. package/dist/zfb-config-gen.js +400 -53
  46. package/package.json +2 -2
  47. package/templates/base/pages/docs/[[...slug]].tsx +59 -0
  48. package/templates/base/pages/index.tsx +6 -41
  49. package/templates/base/src/styles/global.css +15 -340
  50. package/templates/base/tsconfig.json +3 -26
  51. package/templates/features/i18n/files/pages/[locale]/docs/[[...slug]].tsx +88 -0
  52. package/templates/features/tagGovernance/files/scripts/tags-suggest.ts +3 -1
  53. package/dist/settings-gen.d.ts +0 -2
  54. package/dist/settings-gen.js +0 -320
  55. package/templates/base/.htmlvalidate.json +0 -5
  56. package/templates/base/.zfb/doc-history-meta.json +0 -1
  57. package/templates/base/pages/_data.ts +0 -160
  58. package/templates/base/pages/lib/_body-end-islands.tsx +0 -165
  59. package/templates/base/pages/lib/_chrome.ts +0 -167
  60. package/templates/base/pages/lib/_details.tsx +0 -29
  61. package/templates/base/pages/lib/_doc-route-entries.ts +0 -10
  62. package/templates/base/pages/lib/_extract-headings.ts +0 -37
  63. package/templates/base/pages/lib/_frontmatter-preview-data.ts +0 -27
  64. package/templates/base/pages/lib/_nav-source-cache.ts +0 -100
  65. package/templates/base/pages/lib/_nav-source-docs.ts +0 -17
  66. package/templates/base/pages/lib/_preset-generator.tsx +0 -81
  67. package/templates/base/pages/lib/_route-context.ts +0 -32
  68. package/templates/base/pages/lib/_search-widget.tsx +0 -17
  69. package/templates/base/pages/lib/doc-page-props.ts +0 -30
  70. package/templates/base/pages/lib/locale-merge.ts +0 -59
  71. package/templates/base/scripts/run-b4push.sh +0 -102
  72. package/templates/base/src/components/ai-chat-modal.tsx +0 -18
  73. package/templates/base/src/components/content/code-group.tsx +0 -3
  74. package/templates/base/src/components/content/content-admonition.tsx +0 -4
  75. package/templates/base/src/components/desktop-sidebar-toggle.tsx +0 -15
  76. package/templates/base/src/components/doc-history.tsx +0 -21
  77. package/templates/base/src/components/image-enlarge.tsx +0 -24
  78. package/templates/base/src/components/preset-generator.tsx +0 -14
  79. package/templates/base/src/components/sidebar-toggle.tsx +0 -6
  80. package/templates/base/src/components/sidebar-tree.tsx +0 -6
  81. package/templates/base/src/config/color-scheme-utils.ts +0 -69
  82. package/templates/base/src/config/color-schemes.ts +0 -165
  83. package/templates/base/src/config/docs-schema.ts +0 -95
  84. package/templates/base/src/config/frontmatter-preview-defaults.ts +0 -27
  85. package/templates/base/src/config/frontmatter-preview-renderers.tsx +0 -46
  86. package/templates/base/src/config/i18n.ts +0 -239
  87. package/templates/base/src/config/settings-types.ts +0 -45
  88. package/templates/base/src/config/sidebars.ts +0 -66
  89. package/templates/base/src/config/tag-vocabulary-types.ts +0 -4
  90. package/templates/base/src/config/tag-vocabulary.ts +0 -20
  91. package/templates/base/src/config/z-index-tokens.ts +0 -128
  92. package/templates/base/src/types/docs-entry.ts +0 -28
  93. package/templates/base/src/types/heading.ts +0 -5
  94. package/templates/base/src/types/locale.ts +0 -10
  95. package/templates/base/src/utils/base.ts +0 -164
  96. package/templates/base/src/utils/docs.ts +0 -446
  97. package/templates/base/src/utils/git-info.ts +0 -70
  98. package/templates/base/src/utils/github.ts +0 -22
  99. package/templates/base/src/utils/nav-scope.ts +0 -34
  100. package/templates/base/src/utils/sidebar.ts +0 -36
  101. package/templates/base/src/utils/slug.ts +0 -10
  102. package/templates/base/src/utils/smart-break.tsx +0 -12
  103. package/templates/base/src/utils/tags.ts +0 -83
  104. package/templates/base/zfb-shim.d.ts +0 -183
  105. package/templates/features/bodyFootUtil/files/src/utils/github.ts +0 -22
  106. package/templates/features/claudeResources/files/src/integrations/claude-resources/__tests__/escape-for-mdx.test.ts +0 -42
  107. package/templates/features/claudeResources/files/src/integrations/claude-resources/__tests__/generate.test.ts +0 -752
  108. package/templates/features/claudeResources/files/src/integrations/claude-resources/escape-for-mdx.ts +0 -97
  109. package/templates/features/claudeResources/files/src/integrations/claude-resources/generate.ts +0 -735
  110. package/templates/features/designTokenPanel/files/src/components/design-token-panel-bootstrap.tsx +0 -15
  111. package/templates/features/designTokenPanel/files/src/config/design-token-panel-config.ts +0 -435
  112. package/templates/features/designTokenPanel/files/src/config/design-tokens-manifest.ts +0 -174
  113. package/templates/features/designTokenPanel/files/src/lib/design-token-panel-bootstrap.ts +0 -30
  114. package/templates/features/docHistory/files/src/components/doc-history.tsx +0 -10
  115. package/templates/features/docHistory/files/src/types/doc-history.ts +0 -7
  116. package/templates/features/dynamicPageTransition/files/src/components/client-router-bootstrap.tsx +0 -72
  117. package/templates/features/i18n/files/pages/[locale]/index.tsx +0 -72
  118. package/templates/features/imageEnlarge/files/src/components/image-enlarge.tsx +0 -11
  119. package/templates/features/sidebarToggle/files/src/components/desktop-sidebar-toggle.tsx +0 -6
  120. package/templates/features/tauri/files/src/components/find-bar.tsx +0 -122
  121. package/templates/features/tauri/files/src/components/find-in-page-init.tsx +0 -59
  122. package/templates/features/tauri/files/src/utils/find-in-page.ts +0 -175
@@ -1,7 +0,0 @@
1
- // Re-export stub for DocHistory types.
2
- //
3
- // DocHistoryData and DocHistoryEntry now ship from the package at
4
- // @takazudo/zudo-doc/island-types (epic #2344, S1a).
5
- // This file is kept as a stable re-export in case any project-local code
6
- // references the @/types/doc-history path.
7
- export type { DocHistoryData, DocHistoryEntry } from "@takazudo/zudo-doc/island-types";
@@ -1,72 +0,0 @@
1
- "use client";
2
-
3
- // Client-side bootstrap for the @takazudo/zfb-runtime ClientRouter.
4
- //
5
- // Why this exists (zudolab/zudo-doc#1524, W7A verification):
6
- // `<ClientRouter />` (mounted in doc-layout.tsx) emits SSR meta tags + CSS
7
- // for the SPA soft-swap router, but the actual click/form intercept
8
- // registration runs as a top-of-module side effect inside
9
- // `@takazudo/zfb-runtime/src/client-router.ts`:
10
- //
11
- // if (typeof document !== "undefined") { init(); }
12
- //
13
- // The host's existing `import { ClientRouter } from "@takazudo/zfb-runtime"`
14
- // in doc-layout.tsx happens during SSR (where typeof document ===
15
- // "undefined"), so the side effect is silently skipped — the router
16
- // module never reaches the client bundle and every navigation falls
17
- // through to a full page load. W7A's Playwright harness confirmed this:
18
- // pageswap.viewTransition is null, getAnimations() is empty during nav,
19
- // and the sidebar DOM identity is destroyed on every click.
20
- //
21
- // The fix is a minimal "use client" island whose only job is to
22
- // side-effect-import `@takazudo/zfb-runtime/client-router` so the same
23
- // guard fires in the browser. The `init()` is idempotent (guarded by an
24
- // `initialized` flag in router.ts), so even if the import path is
25
- // reached again later the registration runs exactly once.
26
- //
27
- // Bundle cost: the only thing this island ships to the client is the
28
- // client-router subgraph (router.ts + swap-functions.ts + events.ts +
29
- // types.ts + cssesc.ts) plus its single dep on `@takazudo/zfb/runtime`'s
30
- // island manager. The server-only zfb-runtime modules
31
- // (createPageRouter, snapshot, framework adapter) are not transitively
32
- // reachable from the client-router barrel and stay out of the bundle.
33
- //
34
- // Why not call init() in useEffect instead of a side-effect import:
35
- // useEffect fires after Preact mounts, which is after the islands
36
- // runtime fetches and executes the bundle. Module top-level code runs
37
- // as soon as the bundle is parsed by the browser — earlier than mount,
38
- // closer to Astro's <script type="module"> emission timing. The first
39
- // click on a fresh page typically arrives 100ms+ after parse, so this
40
- // timing is what makes the SPA-router actually intercept.
41
-
42
- // Side-effect import — running this file's bundle in the browser
43
- // triggers the `if (typeof document !== "undefined") { init(); }` guard
44
- // at the top of `@takazudo/zfb-runtime/src/client-router.ts`.
45
- import "@takazudo/zfb-runtime/client-router";
46
-
47
- import type { JSX } from "preact";
48
-
49
- /**
50
- * Renders nothing. The island marker exists only so zfb's island scanner
51
- * walks page → BodyEndIslands → ClientRouterBootstrap and includes the
52
- * client-router barrel in the per-island bundle, where the side-effect
53
- * import above can fire on the client.
54
- *
55
- * Note: the SSR `<ClientRouter />` mounted by `DocLayout` (via
56
- * `enableClientRouter`) self-activates the SPA router when its head meta
57
- * tags reach the browser. This host island is therefore a no-op for
58
- * router activation — it is kept only for the `when="load"` hydration
59
- * slot and bundle inclusion. The actual on/off gate is
60
- * `enableClientRouter` on `<DocLayoutWithDefaults>`, set to
61
- * `settings.dynamicPageTransition` at every call site.
62
- */
63
- function ClientRouterBootstrap(): JSX.Element | null {
64
- return null;
65
- }
66
-
67
- // Stable marker name across SSR / scanner / hydration manifest. Required
68
- // for production minification per the existing pattern in
69
- // `pages/lib/_body-end-islands.tsx` (zfb PR #150 marker-name alignment).
70
- ClientRouterBootstrap.displayName = "ClientRouterBootstrap";
71
-
72
- export default ClientRouterBootstrap;
@@ -1,72 +0,0 @@
1
- /** @jsxRuntime automatic */
2
- /** @jsxImportSource preact */
3
- // Page module for the locale-prefixed site index route.
4
- //
5
- // Non-default-locale site index. paths() emits one route per locale defined
6
- // in settings.locales (never the default locale — that is handled by
7
- // pages/index.tsx since prefixDefaultLocale is false).
8
- //
9
- // paths() contract (zfb ADR-004 — synchronous):
10
- // params: { locale: string } — e.g. "ja"
11
- // props: { locale } — resolved locale passed to component
12
- //
13
- // Data flow (inside component — sync per ADR-004):
14
- // routeContext host RouteContext (settings + i18n + nav helpers)
15
- // → prepareHomeData() nav tree, category order, tag count
16
- // → HomePageView renders hero + SiteTreeNav grid + tag section
17
- //
18
- // Thin consumer of `HomePageView` (S3 #2502) and `prepareHomeData` (#2519):
19
- // this file's only job is `paths()` (a locale-routing concern) — mirroring
20
- // the package route's shape (`packages/zudo-doc/src/routes/locale-index.tsx`).
21
- // The locale-specific data prep — `resolveNavSource` with the locale-home
22
- // filter options, `loadCategoryMeta`, and the locale-config guard — now lives
23
- // in `prepareHomeData`. No `extras` here — the showcase's `@Takazudo` brand
24
- // link (#1453) is project-specific and is not part of the generated
25
- // project's hero.
26
-
27
- import { settings } from "@/config/settings";
28
- import { routeContext } from "../lib/_route-context";
29
- import { prepareHomeData } from "@takazudo/zudo-doc/home-page";
30
- import type { JSX } from "preact";
31
- import { HomePageView } from "../lib/_chrome";
32
-
33
- export const frontmatter = { title: "Home" };
34
-
35
- // ---------------------------------------------------------------------------
36
- // paths() — synchronous (ADR-004)
37
- // ---------------------------------------------------------------------------
38
-
39
- /** Emit one route per non-default locale. */
40
- export function paths(): Array<{
41
- params: { locale: string };
42
- props: { locale: string };
43
- }> {
44
- return Object.keys(settings.locales).map((locale) => ({
45
- params: { locale },
46
- props: { locale },
47
- }));
48
- }
49
-
50
- // ---------------------------------------------------------------------------
51
- // Page component
52
- // ---------------------------------------------------------------------------
53
-
54
- interface PageArgs {
55
- params: { locale: string };
56
- props: { locale: string };
57
- }
58
-
59
- export default function LocaleIndexPage({ params }: PageArgs): JSX.Element {
60
- const locale = params.locale;
61
-
62
- const { tree, categoryOrder, tagCount } = prepareHomeData(routeContext, locale);
63
-
64
- return (
65
- <HomePageView
66
- locale={locale}
67
- tree={tree}
68
- categoryOrder={categoryOrder}
69
- tagCount={tagCount}
70
- />
71
- );
72
- }
@@ -1,11 +0,0 @@
1
- "use client";
2
-
3
- // Re-export shim for the ImageEnlarge island.
4
- //
5
- // The real component and its SSR fallback now ship in the package at
6
- // @takazudo/zudo-doc/image-enlarge (epic #2344, S3).
7
- // pages/lib/_body-end-islands.tsx imports them directly from there; this
8
- // overlay overwrites the base W6A no-op stub so that any project-local
9
- // code referencing the @/components/image-enlarge path resolves to the
10
- // real island when the imageEnlarge feature is enabled.
11
- export { ImageEnlarge, ImageEnlargeSsrFallback } from "@takazudo/zudo-doc/image-enlarge";
@@ -1,6 +0,0 @@
1
- // Thin re-export shim. DesktopSidebarToggle moved into the package
2
- // (`@takazudo/zudo-doc/desktop-sidebar-toggle-island`) as part of the
3
- // package-first migration (#2344, S2). Host code using
4
- // `@/components/desktop-sidebar-toggle` still resolves correctly;
5
- // the implementation lives in the package.
6
- export { DesktopSidebarToggle, SIDEBAR_STORAGE_KEY } from "@takazudo/zudo-doc/desktop-sidebar-toggle-island";
@@ -1,122 +0,0 @@
1
- "use client";
2
-
3
- import { useState, useRef, useEffect, useCallback } from "preact/hooks";
4
- import type { FindResult, FindInPage } from "@/utils/find-in-page";
5
-
6
- interface FindBarProps {
7
- visible: boolean;
8
- onClose: () => void;
9
- findInPage: FindInPage;
10
- containerSelector: string;
11
- }
12
-
13
- function toMatchInfo(result: FindResult): FindResult | null {
14
- return result.matches > 0 ? result : null;
15
- }
16
-
17
- export function FindBar({ visible, onClose, findInPage, containerSelector }: FindBarProps) {
18
- const [query, setQuery] = useState("");
19
- const [matchInfo, setMatchInfo] = useState<FindResult | null>(null);
20
- const inputRef = useRef<HTMLInputElement>(null);
21
- // Pin `findInPage` via ref so the visibility-clear effect doesn't
22
- // re-run when a new `find-in-page` instance is passed; the latest
23
- // value is read at effect-fire time without entering the deps.
24
- const findInPageRef = useRef(findInPage);
25
- useEffect(() => {
26
- findInPageRef.current = findInPage;
27
- }, [findInPage]);
28
-
29
- useEffect(() => {
30
- if (visible) {
31
- inputRef.current?.focus();
32
- inputRef.current?.select();
33
- }
34
- }, [visible]);
35
-
36
- useEffect(() => {
37
- if (!visible) {
38
- setQuery("");
39
- setMatchInfo(null);
40
- findInPageRef.current.stop();
41
- }
42
- }, [visible]);
43
-
44
- const handleFind = useCallback(
45
- (text: string) => {
46
- const container = document.querySelector(containerSelector);
47
- if (!text || !(container instanceof HTMLElement)) {
48
- setMatchInfo(null);
49
- findInPage.stop();
50
- return;
51
- }
52
- const result = findInPage.find(container, text);
53
- setMatchInfo(toMatchInfo(result));
54
- },
55
- [findInPage, containerSelector],
56
- );
57
-
58
- const handleKeyDown = useCallback(
59
- (e: React.KeyboardEvent<HTMLInputElement>) => {
60
- if (e.key === "Escape") {
61
- onClose();
62
- } else if (e.key === "Enter") {
63
- const result = e.shiftKey ? findInPage.prev() : findInPage.next();
64
- setMatchInfo(toMatchInfo(result));
65
- }
66
- },
67
- [onClose, findInPage],
68
- );
69
-
70
- if (!visible) return null;
71
-
72
- return (
73
- <div className="fixed top-[3.5rem] right-0 z-dropdown flex items-center gap-hsp-sm py-hsp-xs px-hsp-md bg-surface border-b border-l border-muted rounded-bl-lg shadow-md">
74
- <input
75
- ref={inputRef}
76
- className="w-48 py-[4px] px-hsp-sm rounded text-small bg-bg border border-muted text-fg outline-none focus:border-accent"
77
- type="text"
78
- value={query}
79
- placeholder="Find in page..."
80
- aria-label="Find in page"
81
- onChange={(e) => {
82
- setQuery(e.currentTarget.value);
83
- handleFind(e.currentTarget.value);
84
- }}
85
- onKeyDown={handleKeyDown}
86
- />
87
- <span className="text-caption whitespace-nowrap min-w-[3rem] text-center text-fg/60">
88
- {matchInfo ? `${matchInfo.activeMatchOrdinal}/${matchInfo.matches}` : ""}
89
- </span>
90
- <button
91
- type="button"
92
- className="py-hsp-2xs px-hsp-sm rounded text-caption bg-bg border border-muted text-fg hover:bg-surface"
93
- onClick={() => {
94
- const result = findInPage.prev();
95
- setMatchInfo(toMatchInfo(result));
96
- }}
97
- title="Previous (Shift+Enter)"
98
- >
99
- Prev
100
- </button>
101
- <button
102
- type="button"
103
- className="py-hsp-2xs px-hsp-sm rounded text-caption bg-bg border border-muted text-fg hover:bg-surface"
104
- onClick={() => {
105
- const result = findInPage.next();
106
- setMatchInfo(toMatchInfo(result));
107
- }}
108
- title="Next (Enter)"
109
- >
110
- Next
111
- </button>
112
- <button
113
- type="button"
114
- className="py-hsp-2xs px-hsp-sm rounded text-caption bg-bg border border-muted text-fg hover:bg-surface"
115
- onClick={onClose}
116
- title="Close (Esc)"
117
- >
118
- Close
119
- </button>
120
- </div>
121
- );
122
- }
@@ -1,59 +0,0 @@
1
- "use client";
2
-
3
- import { useState, useEffect, useRef } from "preact/compat";
4
- import { FindBar } from "./find-bar";
5
- import { createFindInPage } from "@/utils/find-in-page";
6
-
7
- const CONTENT_SELECTOR = "article.zd-content";
8
-
9
- export default function FindInPageInit() {
10
- const [isTauri, setIsTauri] = useState(false);
11
- const [visible, setVisible] = useState(false);
12
- const findInPageRef = useRef(createFindInPage());
13
-
14
- // Detect Tauri environment
15
- useEffect(() => {
16
- if (typeof window !== "undefined" && "__TAURI_INTERNALS__" in window) {
17
- setIsTauri(true);
18
- }
19
- }, []);
20
-
21
- // Intercept Cmd/Ctrl+F only in Tauri
22
- useEffect(() => {
23
- if (!isTauri) return;
24
-
25
- const handler = (e: KeyboardEvent) => {
26
- if ((e.metaKey || e.ctrlKey) && e.key === "f") {
27
- e.preventDefault();
28
- setVisible((prev) => !prev);
29
- }
30
- };
31
- document.addEventListener("keydown", handler);
32
- return () => document.removeEventListener("keydown", handler);
33
- }, [isTauri]);
34
-
35
- // Clear search on zfb page navigation. zfb navigates via SPA body swap and
36
- // fires "zfb:before-preparation" on document before nav — it never fires the
37
- // native "pagehide" (full-unload) event. The literal is inlined because
38
- // downstream scaffolds do not depend on @takazudo/zudo-doc as a runtime dep
39
- // (same reason as the designTokenPanel bootstrap).
40
- useEffect(() => {
41
- const handler = () => {
42
- findInPageRef.current.stop();
43
- setVisible(false);
44
- };
45
- document.addEventListener("zfb:before-preparation", handler);
46
- return () => document.removeEventListener("zfb:before-preparation", handler);
47
- }, []);
48
-
49
- if (!isTauri) return null;
50
-
51
- return (
52
- <FindBar
53
- visible={visible}
54
- onClose={() => setVisible(false)}
55
- findInPage={findInPageRef.current}
56
- containerSelector={CONTENT_SELECTOR}
57
- />
58
- );
59
- }
@@ -1,175 +0,0 @@
1
- export interface FindResult {
2
- matches: number;
3
- activeMatchOrdinal: number; // 1-based
4
- }
5
-
6
- export interface FindInPage {
7
- find(container: HTMLElement, query: string): FindResult;
8
- next(): FindResult;
9
- prev(): FindResult;
10
- stop(): void;
11
- }
12
-
13
- const MATCH_CLASS = "find-match";
14
- const ACTIVE_CLASS = "find-match-active";
15
- const MATCH_ATTR = "data-find-match";
16
- const ACTIVE_ATTR = "data-find-active";
17
-
18
- const EMPTY_RESULT: FindResult = Object.freeze({ matches: 0, activeMatchOrdinal: 0 });
19
-
20
- /**
21
- * Create a DOM-based find-in-page utility.
22
- *
23
- * Limitation: only matches within single text nodes. Cross-element matching
24
- * (e.g. "Hello world" spanning `<strong>Hello</strong> world`) is not supported.
25
- */
26
- export function createFindInPage(): FindInPage {
27
- let matchElements: HTMLElement[] = [];
28
- let activeIndex = -1;
29
-
30
- function clearMarks(): void {
31
- const parentsToNormalize = new Set<Node>();
32
-
33
- for (let i = matchElements.length - 1; i >= 0; i--) {
34
- const mark = matchElements[i];
35
- const parent = mark.parentNode;
36
- if (parent) {
37
- const textNode = document.createTextNode(mark.textContent || "");
38
- parent.replaceChild(textNode, mark);
39
- parentsToNormalize.add(parent);
40
- }
41
- }
42
- for (const parent of parentsToNormalize) {
43
- (parent as Element).normalize();
44
- }
45
- matchElements = [];
46
- activeIndex = -1;
47
- }
48
-
49
- function setActive(index: number): void {
50
- if (activeIndex >= 0 && activeIndex < matchElements.length) {
51
- const prev = matchElements[activeIndex];
52
- prev.classList.remove(ACTIVE_CLASS);
53
- prev.removeAttribute(ACTIVE_ATTR);
54
- }
55
- activeIndex = index;
56
- if (activeIndex >= 0 && activeIndex < matchElements.length) {
57
- const current = matchElements[activeIndex];
58
- current.classList.add(ACTIVE_CLASS);
59
- current.setAttribute(ACTIVE_ATTR, "true");
60
- current.scrollIntoView?.({ block: "center" });
61
- }
62
- }
63
-
64
- function currentResult(): FindResult {
65
- if (matchElements.length === 0) {
66
- return EMPTY_RESULT;
67
- }
68
- return {
69
- matches: matchElements.length,
70
- activeMatchOrdinal: activeIndex + 1,
71
- };
72
- }
73
-
74
- function find(container: HTMLElement, query: string): FindResult {
75
- clearMarks();
76
-
77
- if (!query) {
78
- return EMPTY_RESULT;
79
- }
80
-
81
- const lowerQuery = query.toLowerCase();
82
-
83
- const walker = document.createTreeWalker(
84
- container,
85
- NodeFilter.SHOW_TEXT,
86
- null,
87
- );
88
-
89
- const textNodes: Text[] = [];
90
- let node: Text | null;
91
- while ((node = walker.nextNode() as Text | null)) {
92
- textNodes.push(node);
93
- }
94
-
95
- for (const textNode of textNodes) {
96
- const text = textNode.textContent || "";
97
- const lowerText = text.toLowerCase();
98
-
99
- const positions: number[] = [];
100
- let searchFrom = 0;
101
- while (searchFrom < lowerText.length) {
102
- const idx = lowerText.indexOf(lowerQuery, searchFrom);
103
- if (idx === -1) break;
104
- positions.push(idx);
105
- searchFrom = idx + lowerQuery.length;
106
- }
107
-
108
- if (positions.length === 0) continue;
109
-
110
- const parent = textNode.parentNode;
111
- if (!parent) continue;
112
-
113
- let remainingNode: Text = textNode;
114
- const nodeMarks: HTMLElement[] = [];
115
-
116
- for (let i = positions.length - 1; i >= 0; i--) {
117
- const pos = positions[i];
118
- const matchLen = query.length;
119
-
120
- if (pos + matchLen < remainingNode.length) {
121
- remainingNode.splitText(pos + matchLen);
122
- }
123
-
124
- let matchNode: Text;
125
- if (pos > 0) {
126
- matchNode = remainingNode.splitText(pos);
127
- } else {
128
- matchNode = remainingNode;
129
- }
130
-
131
- const mark = document.createElement("mark");
132
- mark.className = MATCH_CLASS;
133
- mark.setAttribute(MATCH_ATTR, "true");
134
- mark.textContent = matchNode.textContent;
135
-
136
- parent.replaceChild(mark, matchNode);
137
- nodeMarks.unshift(mark);
138
- }
139
-
140
- matchElements.push(...nodeMarks);
141
- }
142
-
143
- if (matchElements.length === 0) {
144
- return EMPTY_RESULT;
145
- }
146
-
147
- setActive(0);
148
- return currentResult();
149
- }
150
-
151
- function next(): FindResult {
152
- if (matchElements.length === 0) {
153
- return EMPTY_RESULT;
154
- }
155
- const newIndex = (activeIndex + 1) % matchElements.length;
156
- setActive(newIndex);
157
- return currentResult();
158
- }
159
-
160
- function prev(): FindResult {
161
- if (matchElements.length === 0) {
162
- return EMPTY_RESULT;
163
- }
164
- const newIndex =
165
- (activeIndex - 1 + matchElements.length) % matchElements.length;
166
- setActive(newIndex);
167
- return currentResult();
168
- }
169
-
170
- function stop(): void {
171
- clearMarks();
172
- }
173
-
174
- return { find, next, prev, stop };
175
- }