create-zudo-doc 0.1.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.
- package/LICENSE +21 -0
- package/README.md +146 -0
- package/bin/create-zudo-doc.js +2 -0
- package/dist/api.d.ts +20 -0
- package/dist/api.js +13 -0
- package/dist/claude-md-gen.d.ts +2 -0
- package/dist/claude-md-gen.js +113 -0
- package/dist/cli.d.ts +39 -0
- package/dist/cli.js +157 -0
- package/dist/compose.d.ts +95 -0
- package/dist/compose.js +206 -0
- package/dist/constants.d.ts +20 -0
- package/dist/constants.js +224 -0
- package/dist/features/body-foot-util.d.ts +10 -0
- package/dist/features/body-foot-util.js +12 -0
- package/dist/features/claude-resources.d.ts +2 -0
- package/dist/features/claude-resources.js +6 -0
- package/dist/features/design-token-panel.d.ts +14 -0
- package/dist/features/design-token-panel.js +27 -0
- package/dist/features/doc-history.d.ts +9 -0
- package/dist/features/doc-history.js +11 -0
- package/dist/features/doc-tags.d.ts +19 -0
- package/dist/features/doc-tags.js +33 -0
- package/dist/features/footer-taglist.d.ts +14 -0
- package/dist/features/footer-taglist.js +17 -0
- package/dist/features/footer.d.ts +8 -0
- package/dist/features/footer.js +10 -0
- package/dist/features/i18n.d.ts +22 -0
- package/dist/features/i18n.js +41 -0
- package/dist/features/image-enlarge.d.ts +11 -0
- package/dist/features/image-enlarge.js +13 -0
- package/dist/features/index.d.ts +15 -0
- package/dist/features/index.js +53 -0
- package/dist/features/llms-txt.d.ts +11 -0
- package/dist/features/llms-txt.js +13 -0
- package/dist/features/search.d.ts +9 -0
- package/dist/features/search.js +11 -0
- package/dist/features/sidebar-resizer.d.ts +14 -0
- package/dist/features/sidebar-resizer.js +16 -0
- package/dist/features/sidebar-toggle.d.ts +13 -0
- package/dist/features/sidebar-toggle.js +15 -0
- package/dist/features/tag-governance.d.ts +14 -0
- package/dist/features/tag-governance.js +16 -0
- package/dist/features/tauri-dev.d.ts +2 -0
- package/dist/features/tauri-dev.js +25 -0
- package/dist/features/tauri.d.ts +11 -0
- package/dist/features/tauri.js +52 -0
- package/dist/features/versioning.d.ts +27 -0
- package/dist/features/versioning.js +43 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +150 -0
- package/dist/preset.d.ts +37 -0
- package/dist/preset.js +156 -0
- package/dist/prompts.d.ts +32 -0
- package/dist/prompts.js +248 -0
- package/dist/scaffold.d.ts +4 -0
- package/dist/scaffold.js +344 -0
- package/dist/settings-gen.d.ts +2 -0
- package/dist/settings-gen.js +237 -0
- package/dist/utils.d.ts +8 -0
- package/dist/utils.js +34 -0
- package/dist/zfb-config-gen.d.ts +19 -0
- package/dist/zfb-config-gen.js +222 -0
- package/package.json +65 -0
- package/templates/base/.htmlvalidate.json +5 -0
- package/templates/base/.zfb/doc-history-meta.json +1 -0
- package/templates/base/pages/404.tsx +55 -0
- package/templates/base/pages/_data.ts +179 -0
- package/templates/base/pages/_mdx-components.ts +249 -0
- package/templates/base/pages/docs/[...slug].tsx +448 -0
- package/templates/base/pages/index.tsx +158 -0
- package/templates/base/pages/lib/_body-end-islands.tsx +201 -0
- package/templates/base/pages/lib/_category-nav.tsx +148 -0
- package/templates/base/pages/lib/_category-tree-nav.tsx +104 -0
- package/templates/base/pages/lib/_compose-meta-title.ts +29 -0
- package/templates/base/pages/lib/_details.tsx +30 -0
- package/templates/base/pages/lib/_doc-history-area.tsx +178 -0
- package/templates/base/pages/lib/_doc-metainfo-area.tsx +100 -0
- package/templates/base/pages/lib/_doc-tags-area.tsx +89 -0
- package/templates/base/pages/lib/_extract-headings.ts +81 -0
- package/templates/base/pages/lib/_footer-with-defaults.tsx +234 -0
- package/templates/base/pages/lib/_frontmatter-preview-data.ts +53 -0
- package/templates/base/pages/lib/_head-with-defaults.tsx +113 -0
- package/templates/base/pages/lib/_header-with-defaults.tsx +386 -0
- package/templates/base/pages/lib/_inline-version-switcher.tsx +84 -0
- package/templates/base/pages/lib/_math-block.tsx +63 -0
- package/templates/base/pages/lib/_nav-source-docs.ts +68 -0
- package/templates/base/pages/lib/_preset-generator.tsx +81 -0
- package/templates/base/pages/lib/_search-widget-script.ts +388 -0
- package/templates/base/pages/lib/_search-widget.tsx +196 -0
- package/templates/base/pages/lib/_sidebar-with-defaults.tsx +176 -0
- package/templates/base/pages/lib/_site-tree-nav.tsx +128 -0
- package/templates/base/pages/lib/locale-merge.ts +58 -0
- package/templates/base/pages/lib/route-enumerators.ts +302 -0
- package/templates/base/pages/sitemap.xml.tsx +51 -0
- package/templates/base/plugins/connect-adapter.mjs +144 -0
- package/templates/base/plugins/copy-public-plugin.mjs +50 -0
- package/templates/base/plugins/search-index-plugin.mjs +54 -0
- package/templates/base/scripts/run-b4push.sh +102 -0
- package/templates/base/src/components/ai-chat-modal.tsx +15 -0
- package/templates/base/src/components/client-router-bootstrap.tsx +14 -0
- package/templates/base/src/components/content/component-map.ts +25 -0
- package/templates/base/src/components/content/content-blockquote.tsx +16 -0
- package/templates/base/src/components/content/content-code.tsx +117 -0
- package/templates/base/src/components/content/content-link.tsx +83 -0
- package/templates/base/src/components/content/content-ol.tsx +19 -0
- package/templates/base/src/components/content/content-paragraph.tsx +10 -0
- package/templates/base/src/components/content/content-strong.tsx +16 -0
- package/templates/base/src/components/content/content-table.tsx +18 -0
- package/templates/base/src/components/content/content-ul.tsx +18 -0
- package/templates/base/src/components/content/heading-h2.tsx +26 -0
- package/templates/base/src/components/content/heading-h3.tsx +26 -0
- package/templates/base/src/components/content/heading-h4.tsx +26 -0
- package/templates/base/src/components/design-token-panel-bootstrap.tsx +15 -0
- package/templates/base/src/components/desktop-sidebar-toggle.tsx +15 -0
- package/templates/base/src/components/doc-history.tsx +18 -0
- package/templates/base/src/components/html-preview/highlighted-code.tsx +74 -0
- package/templates/base/src/components/html-preview/html-preview.tsx +108 -0
- package/templates/base/src/components/html-preview/preflight.ts +112 -0
- package/templates/base/src/components/html-preview/preview-base.tsx +159 -0
- package/templates/base/src/components/image-enlarge.tsx +19 -0
- package/templates/base/src/components/mobile-toc.tsx +94 -0
- package/templates/base/src/components/preset-generator.tsx +14 -0
- package/templates/base/src/components/sidebar-toggle.tsx +98 -0
- package/templates/base/src/components/sidebar-tree.tsx +543 -0
- package/templates/base/src/components/site-tree-nav.tsx +233 -0
- package/templates/base/src/components/theme-toggle.tsx +93 -0
- package/templates/base/src/components/toc.tsx +63 -0
- package/templates/base/src/components/tree-nav-shared.tsx +71 -0
- package/templates/base/src/config/color-scheme-utils.ts +182 -0
- package/templates/base/src/config/color-schemes.ts +128 -0
- package/templates/base/src/config/frontmatter-preview-defaults.ts +24 -0
- package/templates/base/src/config/frontmatter-preview-renderers.tsx +46 -0
- package/templates/base/src/config/i18n.ts +225 -0
- package/templates/base/src/config/settings-types.ts +162 -0
- package/templates/base/src/config/sidebars.ts +66 -0
- package/templates/base/src/config/tag-vocabulary-types.ts +39 -0
- package/templates/base/src/config/tag-vocabulary.ts +20 -0
- package/templates/base/src/hooks/use-active-heading.ts +133 -0
- package/templates/base/src/plugins/docs-source-map.ts +103 -0
- package/templates/base/src/plugins/hast-utils.ts +10 -0
- package/templates/base/src/plugins/rehype-code-title.ts +50 -0
- package/templates/base/src/plugins/rehype-heading-links.ts +53 -0
- package/templates/base/src/plugins/rehype-image-enlarge.ts +113 -0
- package/templates/base/src/plugins/rehype-mermaid.ts +41 -0
- package/templates/base/src/plugins/rehype-strip-md-extension.ts +58 -0
- package/templates/base/src/plugins/remark-admonitions.ts +99 -0
- package/templates/base/src/plugins/remark-resolve-markdown-links.ts +127 -0
- package/templates/base/src/plugins/url-utils.ts +4 -0
- package/templates/base/src/styles/global.css +1066 -0
- package/templates/base/src/types/docs-entry.ts +39 -0
- package/templates/base/src/types/heading.ts +5 -0
- package/templates/base/src/types/locale.ts +10 -0
- package/templates/base/src/utils/base.ts +139 -0
- package/templates/base/src/utils/content-files.ts +106 -0
- package/templates/base/src/utils/dedent.ts +24 -0
- package/templates/base/src/utils/docs.ts +335 -0
- package/templates/base/src/utils/git-info.ts +70 -0
- package/templates/base/src/utils/github.ts +19 -0
- package/templates/base/src/utils/header-right-items.ts +38 -0
- package/templates/base/src/utils/nav-scope.ts +63 -0
- package/templates/base/src/utils/sidebar.ts +104 -0
- package/templates/base/src/utils/slug.ts +10 -0
- package/templates/base/src/utils/smart-break.tsx +126 -0
- package/templates/base/src/utils/tags.ts +126 -0
- package/templates/base/tsconfig.json +36 -0
- package/templates/features/bodyFootUtil/files/src/utils/github.ts +19 -0
- package/templates/features/claudeResources/files/plugins/claude-resources-plugin.mjs +137 -0
- package/templates/features/claudeResources/files/src/integrations/claude-resources/__tests__/escape-for-mdx.test.ts +34 -0
- package/templates/features/claudeResources/files/src/integrations/claude-resources/__tests__/generate.test.ts +376 -0
- package/templates/features/claudeResources/files/src/integrations/claude-resources/escape-for-mdx.ts +93 -0
- package/templates/features/claudeResources/files/src/integrations/claude-resources/generate.ts +586 -0
- package/templates/features/designTokenPanel/files/src/components/design-token-panel-bootstrap.tsx +15 -0
- package/templates/features/designTokenPanel/files/src/config/design-token-panel-config.ts +99 -0
- package/templates/features/designTokenPanel/files/src/config/design-tokens-manifest.ts +177 -0
- package/templates/features/designTokenPanel/files/src/lib/design-token-panel-bootstrap.ts +50 -0
- package/templates/features/docHistory/files/plugins/doc-history-plugin.mjs +99 -0
- package/templates/features/docHistory/files/src/components/doc-history.tsx +598 -0
- package/templates/features/docHistory/files/src/types/doc-history.ts +23 -0
- package/templates/features/docHistory/files/src/utils/doc-history.ts +180 -0
- package/templates/features/docTags/files/pages/[locale]/docs/tags/[tag].tsx +116 -0
- package/templates/features/docTags/files/pages/[locale]/docs/tags/index.tsx +99 -0
- package/templates/features/docTags/files/pages/docs/tags/[tag].tsx +101 -0
- package/templates/features/docTags/files/pages/docs/tags/index.tsx +86 -0
- package/templates/features/i18n/files/pages/[locale]/docs/[...slug].tsx +467 -0
- package/templates/features/i18n/files/pages/[locale]/index.tsx +213 -0
- package/templates/features/imageEnlarge/files/src/components/image-enlarge.tsx +248 -0
- package/templates/features/llmsTxt/files/plugins/llms-txt-plugin.mjs +74 -0
- package/templates/features/sidebarResizer/files/src/scripts/sidebar-resizer.ts +185 -0
- package/templates/features/sidebarToggle/files/src/components/desktop-sidebar-toggle.tsx +126 -0
- package/templates/features/tagGovernance/files/scripts/tags-audit.ts +576 -0
- package/templates/features/tagGovernance/files/scripts/tags-suggest.ts +428 -0
- package/templates/features/tauri/files/src/components/find-bar.tsx +122 -0
- package/templates/features/tauri/files/src/components/find-in-page-init.tsx +53 -0
- package/templates/features/tauri/files/src/utils/find-in-page.ts +175 -0
- package/templates/features/tauri/files/src-tauri/Cargo.toml +14 -0
- package/templates/features/tauri/files/src-tauri/build.rs +3 -0
- package/templates/features/tauri/files/src-tauri/capabilities/default.json +11 -0
- package/templates/features/tauri/files/src-tauri/src/main.rs +250 -0
- package/templates/features/tauri/files/src-tauri/tauri.conf.json +25 -0
- package/templates/features/tauriDev/files/src-tauri-dev/Cargo.toml +15 -0
- package/templates/features/tauriDev/files/src-tauri-dev/build.rs +3 -0
- package/templates/features/tauriDev/files/src-tauri-dev/capabilities/default.json +7 -0
- package/templates/features/tauriDev/files/src-tauri-dev/frontend/index.html +187 -0
- package/templates/features/tauriDev/files/src-tauri-dev/icons/icon.png +0 -0
- package/templates/features/tauriDev/files/src-tauri-dev/src/main.rs +995 -0
- package/templates/features/tauriDev/files/src-tauri-dev/tauri.conf.json +22 -0
- package/templates/features/tauriDev/files/src-tauri-dev/test-launch.sh +65 -0
- package/templates/features/versioning/files/pages/[locale]/docs/versions.tsx +100 -0
- package/templates/features/versioning/files/pages/docs/versions.tsx +78 -0
- package/templates/features/versioning/files/pages/v/[version]/docs/[...slug].tsx +451 -0
- package/templates/features/versioning/files/pages/v/[version]/ja/docs/[...slug].tsx +490 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/** @jsxRuntime automatic */
|
|
2
|
+
/** @jsxImportSource preact */
|
|
3
|
+
// MathBlock — server-rendered KaTeX component for MDX math expressions.
|
|
4
|
+
//
|
|
5
|
+
// Registered in pages/_mdx-components.ts as `MathBlock` so the MDX corpus
|
|
6
|
+
// can reference it as <MathBlock latex="…" block />.
|
|
7
|
+
//
|
|
8
|
+
// Used by the math-equations.mdx content files (both EN and JA) which write
|
|
9
|
+
// `<MathBlock>` JSX directly instead of `$$…$$` fences. The explicit JSX
|
|
10
|
+
// form is required because the zfb Rust MDX→JSX emitter does not understand
|
|
11
|
+
// remark-math `$$…$$` syntax — LaTeX identifiers like `\infty` become invalid
|
|
12
|
+
// JSX expressions `{\infty}` that esbuild rejects (zudo-front-builder #93).
|
|
13
|
+
// Using `<MathBlock>` directly keeps the LaTeX inside a string attribute,
|
|
14
|
+
// which esbuild accepts cleanly.
|
|
15
|
+
//
|
|
16
|
+
// Rendering: katex.renderToString() is called at SSR time — no client JS.
|
|
17
|
+
// `throwOnError: false` keeps a broken formula visible as an error span
|
|
18
|
+
// rather than crashing the page.
|
|
19
|
+
|
|
20
|
+
import katex from "katex";
|
|
21
|
+
import type { VNode } from "preact";
|
|
22
|
+
|
|
23
|
+
interface MathBlockProps {
|
|
24
|
+
/** Raw LaTeX source string. */
|
|
25
|
+
latex: string;
|
|
26
|
+
/** When true, renders as a block (display) equation; otherwise inline. */
|
|
27
|
+
block?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Server-rendered KaTeX math component.
|
|
32
|
+
*
|
|
33
|
+
* Block mode wraps the output in `<div class="math math-display">`;
|
|
34
|
+
* inline mode uses `<span class="math math-inline">`. The class names
|
|
35
|
+
* match the standard rehype-katex output so existing CSS (e.g. the
|
|
36
|
+
* KaTeX stylesheet) still applies.
|
|
37
|
+
*/
|
|
38
|
+
export function MathBlock({ latex, block = false }: MathBlockProps): VNode {
|
|
39
|
+
const html = katex.renderToString(latex, {
|
|
40
|
+
displayMode: block,
|
|
41
|
+
// Never throw — malformed LaTeX renders a visible error span instead
|
|
42
|
+
// of crashing the entire page build.
|
|
43
|
+
throwOnError: false,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (block) {
|
|
47
|
+
return (
|
|
48
|
+
<div
|
|
49
|
+
class="math math-display"
|
|
50
|
+
// eslint-disable-next-line react/no-danger
|
|
51
|
+
dangerouslySetInnerHTML={{ __html: html }}
|
|
52
|
+
/>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<span
|
|
58
|
+
class="math math-inline"
|
|
59
|
+
// eslint-disable-next-line react/no-danger
|
|
60
|
+
dangerouslySetInnerHTML={{ __html: html }}
|
|
61
|
+
/>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Shared helper — pick the right loadDocs() collection and category-meta dir
|
|
2
|
+
// for the active (locale, version) pair, applying the locale-first + EN-fallback
|
|
3
|
+
// merge that pages/[locale]/docs/[...slug].tsx uses in its own paths() pass so
|
|
4
|
+
// the sidebar tree mirrors what those pages enumerate.
|
|
5
|
+
//
|
|
6
|
+
// Used by _sidebar-with-defaults.tsx (desktop sidebar) and
|
|
7
|
+
// _header-with-defaults.tsx (mobile SidebarToggle) so both nav surfaces apply
|
|
8
|
+
// the same defaultLocaleOnlyPrefixes filter and stay in sync.
|
|
9
|
+
|
|
10
|
+
import { defaultLocale, type Locale } from "@/config/i18n";
|
|
11
|
+
import { settings } from "@/config/settings";
|
|
12
|
+
import { docsUrl, isDefaultLocaleOnlyPath } from "@/utils/base";
|
|
13
|
+
import { loadCategoryMeta, type CategoryMeta } from "@/utils/docs";
|
|
14
|
+
import type { DocsEntry } from "@/types/docs-entry";
|
|
15
|
+
import { loadDocs } from "../_data";
|
|
16
|
+
|
|
17
|
+
export type NavSourceDocs = {
|
|
18
|
+
docs: DocsEntry[];
|
|
19
|
+
categoryMeta: Map<string, CategoryMeta>;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Pick the right `loadDocs(...)` collection name and category-meta dir
|
|
24
|
+
* for the active (locale, version) pair, applying the same locale-first
|
|
25
|
+
* + EN-fallback merge that `pages/[locale]/docs/[...slug].tsx` performs
|
|
26
|
+
* in its own `paths()` so the sidebar tree mirrors what those pages
|
|
27
|
+
* enumerate.
|
|
28
|
+
*/
|
|
29
|
+
export function loadNavSourceDocs(
|
|
30
|
+
lang: Locale,
|
|
31
|
+
currentVersion: string | undefined,
|
|
32
|
+
): NavSourceDocs {
|
|
33
|
+
if (currentVersion) {
|
|
34
|
+
const collectionName = `docs-v-${currentVersion}`;
|
|
35
|
+
const versionConfig = settings.versions?.find((v) => v.slug === currentVersion);
|
|
36
|
+
const docs = loadDocs(collectionName).filter((d) => !d.data.draft);
|
|
37
|
+
const categoryMeta = loadCategoryMeta(versionConfig?.docsDir ?? settings.docsDir);
|
|
38
|
+
return { docs, categoryMeta };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (lang === defaultLocale) {
|
|
42
|
+
const docs = loadDocs("docs").filter((d) => !d.data.draft);
|
|
43
|
+
const categoryMeta = loadCategoryMeta(settings.docsDir);
|
|
44
|
+
return { docs, categoryMeta };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Non-default locale: locale-first merge with EN fallback so docs the
|
|
48
|
+
// active locale has not yet translated still appear in the tree.
|
|
49
|
+
const localeDocs = loadDocs(`docs-${lang}`).filter((d) => !d.data.draft);
|
|
50
|
+
const baseDocs = loadDocs("docs").filter((d) => !d.data.draft);
|
|
51
|
+
const localeSlugSet = new Set(localeDocs.map((d) => d.data.slug ?? d.id));
|
|
52
|
+
const fallbackDocs = baseDocs
|
|
53
|
+
.filter((d) => !localeSlugSet.has(d.data.slug ?? d.id))
|
|
54
|
+
.filter((d) => !isDefaultLocaleOnlyPath(docsUrl(d.data.slug ?? d.id)));
|
|
55
|
+
const allDocs = [...localeDocs, ...fallbackDocs];
|
|
56
|
+
|
|
57
|
+
const localeDir =
|
|
58
|
+
(settings.locales as Record<string, { dir?: string }>)[lang]?.dir ??
|
|
59
|
+
settings.docsDir;
|
|
60
|
+
// Base meta first, locale meta wins on overlapping keys — same merge
|
|
61
|
+
// order [locale]/docs/[...slug].tsx uses in its paths() pass.
|
|
62
|
+
const categoryMeta = new Map<string, CategoryMeta>([
|
|
63
|
+
...loadCategoryMeta(settings.docsDir),
|
|
64
|
+
...loadCategoryMeta(localeDir),
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
return { docs: allDocs, categoryMeta };
|
|
68
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/** @jsxRuntime automatic */
|
|
2
|
+
/** @jsxImportSource preact */
|
|
3
|
+
// SSR fallback shell for the <PresetGenerator> interactive form.
|
|
4
|
+
//
|
|
5
|
+
// The real component (src/components/preset-generator.tsx) is a large
|
|
6
|
+
// client-only island. This file is imported transitively from page modules
|
|
7
|
+
// (pages/docs/[...slug].tsx → _mdx-components.ts → here), so zfb's island
|
|
8
|
+
// scanner walks the static import chain and registers PresetGenerator in the
|
|
9
|
+
// manifest. Without this import, the scanner never finds the component and
|
|
10
|
+
// client-side hydration never fires (orphan-component problem; same root
|
|
11
|
+
// cause fixed for body-end islands in _body-end-islands.tsx).
|
|
12
|
+
//
|
|
13
|
+
// The fallback renders all 8 section headings as static SSR HTML so:
|
|
14
|
+
// 1. Screen readers and search engines see the section structure (a11y/SEO).
|
|
15
|
+
// 2. Layout does not collapse to nothing while JS loads (no-JS layout).
|
|
16
|
+
// 3. The scanner traces this file → preset-generator.tsx via the Island child
|
|
17
|
+
// and registers PresetGenerator in the manifest for client-side mounting.
|
|
18
|
+
//
|
|
19
|
+
// Uses the canonical `<Island ssrFallback>` API (zfb) so the scanner can
|
|
20
|
+
// connect the import to the manifest entry and the hydration runtime can
|
|
21
|
+
// mount the real form into the skip-ssr placeholder on the client.
|
|
22
|
+
|
|
23
|
+
import type { VNode } from "preact";
|
|
24
|
+
import { HeadingH3 } from "@takazudo/zudo-doc/content";
|
|
25
|
+
import { Island } from "@takazudo/zfb";
|
|
26
|
+
import PresetGenerator from "@/components/preset-generator";
|
|
27
|
+
|
|
28
|
+
// Pin displayName so zfb's captureComponentName produces a stable marker
|
|
29
|
+
// name even after the SSR pipeline runs through a function-name-rewriting
|
|
30
|
+
// layer. Matches the data-zfb-island-skip-ssr attribute value the runtime
|
|
31
|
+
// queries. Mirrors the pattern in _body-end-islands.tsx.
|
|
32
|
+
(PresetGenerator as { displayName?: string }).displayName = "PresetGenerator";
|
|
33
|
+
|
|
34
|
+
// Heading text for each of the 8 sections — must match the original
|
|
35
|
+
// SectionHeading calls in src/components/preset-generator.tsx exactly
|
|
36
|
+
// so the SSR fallback and the real component render the same section labels.
|
|
37
|
+
// Order must mirror the JSX source order in preset-generator.tsx — do NOT
|
|
38
|
+
// sort alphabetically. The array drives the SSR fallback heading sequence
|
|
39
|
+
// shown to screen readers and visible before JS hydration.
|
|
40
|
+
const SECTION_HEADINGS = [
|
|
41
|
+
"Project Name",
|
|
42
|
+
"Default Language",
|
|
43
|
+
"Color Scheme Mode",
|
|
44
|
+
"Color Scheme",
|
|
45
|
+
"Features",
|
|
46
|
+
"Header right items",
|
|
47
|
+
"Markdown Options",
|
|
48
|
+
"Package Manager",
|
|
49
|
+
] as const;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Static SSR fallback for the interactive PresetGenerator form.
|
|
53
|
+
*
|
|
54
|
+
* Renders all 8 section headings as static HTML for a11y/SEO and no-JS
|
|
55
|
+
* layout stability. Uses Island with ssrFallback so the zfb scanner traces
|
|
56
|
+
* this file → preset-generator.tsx and registers the real component in the
|
|
57
|
+
* island manifest for client-side mounting.
|
|
58
|
+
*/
|
|
59
|
+
export function PresetGeneratorFallback(): VNode {
|
|
60
|
+
const fallback = (
|
|
61
|
+
<div class="zd-preset-gen-fallback">
|
|
62
|
+
{SECTION_HEADINGS.map((heading) => (
|
|
63
|
+
<section key={heading}>
|
|
64
|
+
<HeadingH3>{heading}</HeadingH3>
|
|
65
|
+
</section>
|
|
66
|
+
))}
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
// Island with ssrFallback:
|
|
71
|
+
// - SSR emits the section headings as static HTML inside the skip-ssr div.
|
|
72
|
+
// - The scanner reads children.type = PresetGenerator → registers it in
|
|
73
|
+
// the manifest under "PresetGenerator".
|
|
74
|
+
// - The hydration runtime mounts the real interactive form into the
|
|
75
|
+
// skip-ssr placeholder on the client after load.
|
|
76
|
+
return Island({
|
|
77
|
+
when: "load",
|
|
78
|
+
ssrFallback: fallback,
|
|
79
|
+
children: <PresetGenerator />,
|
|
80
|
+
}) as unknown as VNode;
|
|
81
|
+
}
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
// Client-side script for the SiteSearch custom element.
|
|
2
|
+
//
|
|
3
|
+
// Port of the `<script>` block from the deleted
|
|
4
|
+
// `src/components/search.astro` (commit a4d9956). Rewritten as a plain
|
|
5
|
+
// JavaScript IIFE (no ES-module `import` statements) so it can be
|
|
6
|
+
// emitted via `dangerouslySetInnerHTML` without requiring bundler
|
|
7
|
+
// support for inline scripts.
|
|
8
|
+
//
|
|
9
|
+
// Differences from the original Astro script:
|
|
10
|
+
// - MiniSearch is NOT imported; a lightweight built-in search (fetch
|
|
11
|
+
// index + simple word-match scoring) is used instead. This avoids the
|
|
12
|
+
// inline-script bundling limitation. Full MiniSearch integration can be
|
|
13
|
+
// added in a follow-up topic once the bundle pipeline is in place.
|
|
14
|
+
// - The post-navigation rebinder pulls its event name from
|
|
15
|
+
// `AFTER_NAVIGATE_EVENT` in
|
|
16
|
+
// `@takazudo/zudo-doc/transitions` (today: `zfb:after-swap`)
|
|
17
|
+
// rather than a hard-coded `astro:*` literal. See
|
|
18
|
+
// zudolab/zudo-doc#1335 (E2 task 2 half B) for the vocabulary
|
|
19
|
+
// introduction and zudolab/zudo-doc#1523 for the W6B flip from
|
|
20
|
+
// `DOMContentLoaded` to the Strategy B SPA event name.
|
|
21
|
+
|
|
22
|
+
import { AFTER_NAVIGATE_EVENT } from "@takazudo/zudo-doc/transitions";
|
|
23
|
+
|
|
24
|
+
export const SEARCH_WIDGET_SCRIPT = /* javascript */ `(function () {
|
|
25
|
+
if (customElements.get("site-search")) return; // guard double-registration
|
|
26
|
+
|
|
27
|
+
var PAGE_SIZE = 10;
|
|
28
|
+
|
|
29
|
+
function escapeHtml(text) {
|
|
30
|
+
return String(text)
|
|
31
|
+
.replace(/&/g, "&")
|
|
32
|
+
.replace(/</g, "<")
|
|
33
|
+
.replace(/>/g, ">")
|
|
34
|
+
.replace(/"/g, """)
|
|
35
|
+
.replace(/'/g, "'");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function escapeRegExp(text) {
|
|
39
|
+
return text.replace(/[.*+?^\${}()|[\\]\\\\]/g, "\\\\$&");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function parseTerms(query) {
|
|
43
|
+
return query.trim().split(/\\s+/).filter(Boolean);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function scoreEntry(entry, terms) {
|
|
47
|
+
var score = 0;
|
|
48
|
+
var titleLower = (entry.title || "").toLowerCase();
|
|
49
|
+
var bodyLower = (entry.body || "").toLowerCase();
|
|
50
|
+
var descLower = (entry.description || "").toLowerCase();
|
|
51
|
+
for (var i = 0; i < terms.length; i++) {
|
|
52
|
+
var t = terms[i].toLowerCase();
|
|
53
|
+
if (titleLower.indexOf(t) !== -1) score += 3;
|
|
54
|
+
if (descLower.indexOf(t) !== -1) score += 2;
|
|
55
|
+
if (bodyLower.indexOf(t) !== -1) score += 1;
|
|
56
|
+
}
|
|
57
|
+
return score;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function highlightTerms(text, terms) {
|
|
61
|
+
if (!terms.length) return escapeHtml(text);
|
|
62
|
+
var escaped = terms.map(function(t) { return escapeRegExp(t); });
|
|
63
|
+
var pattern = new RegExp("(" + escaped.join("|") + ")", "gi");
|
|
64
|
+
return text.split(pattern).map(function(seg, i) {
|
|
65
|
+
return i % 2 === 1
|
|
66
|
+
? "<mark>" + escapeHtml(seg) + "</mark>"
|
|
67
|
+
: escapeHtml(seg);
|
|
68
|
+
}).join("");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function truncate(text, query, max) {
|
|
72
|
+
max = max || 200;
|
|
73
|
+
if (text.length <= max) return text;
|
|
74
|
+
var terms = parseTerms(query);
|
|
75
|
+
var lower = text.toLowerCase();
|
|
76
|
+
var best = -1;
|
|
77
|
+
for (var i = 0; i < terms.length; i++) {
|
|
78
|
+
var idx = lower.indexOf(terms[i].toLowerCase());
|
|
79
|
+
if (idx !== -1 && (best === -1 || idx < best)) best = idx;
|
|
80
|
+
}
|
|
81
|
+
if (best === -1) return text.slice(0, max) + "\\u2026";
|
|
82
|
+
var half = Math.floor(max / 2);
|
|
83
|
+
var start = Math.max(0, best - half);
|
|
84
|
+
var end = start + max;
|
|
85
|
+
if (end > text.length) { end = text.length; start = Math.max(0, end - max); }
|
|
86
|
+
var result = text.slice(start, end);
|
|
87
|
+
if (start > 0) result = "\\u2026" + result;
|
|
88
|
+
if (end < text.length) result += "\\u2026";
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
customElements.define("site-search", class SiteSearch extends HTMLElement {
|
|
93
|
+
constructor() {
|
|
94
|
+
super();
|
|
95
|
+
this._dialog = null;
|
|
96
|
+
this._openBtn = null;
|
|
97
|
+
this._closeBtn = null;
|
|
98
|
+
this._input = null;
|
|
99
|
+
this._results = null;
|
|
100
|
+
this._countWide = null;
|
|
101
|
+
this._countNarrow = null;
|
|
102
|
+
this._entries = null;
|
|
103
|
+
this._loading = false;
|
|
104
|
+
this._debounce = null;
|
|
105
|
+
this._currentQuery = "";
|
|
106
|
+
this._allResults = [];
|
|
107
|
+
this._shownCount = 0;
|
|
108
|
+
this._shortcut = "";
|
|
109
|
+
this._resultCountTemplate = "";
|
|
110
|
+
this._keydownHandler = null;
|
|
111
|
+
this._observer = null;
|
|
112
|
+
this._sentinel = null;
|
|
113
|
+
this._isLoadingBatch = false;
|
|
114
|
+
// Snapshot of the initial results-area HTML (includes SSR placeholder).
|
|
115
|
+
// Captured in connectedCallback so we can restore it on input-clear
|
|
116
|
+
// without re-querying the DOM (the placeholder node is replaced once
|
|
117
|
+
// search results are rendered).
|
|
118
|
+
this._placeholderHtml = "";
|
|
119
|
+
// Held so we can remove the document-level after-navigate listener
|
|
120
|
+
// in disconnectedCallback. zudolab/zudo-doc#1523 — under Strategy B
|
|
121
|
+
// SPA navigation a non-persisted <site-search> element would leak
|
|
122
|
+
// one document listener per nav without this hook.
|
|
123
|
+
this._afterNavHandler = null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
connectedCallback() {
|
|
127
|
+
this._dialog = this.querySelector("[data-search-dialog]");
|
|
128
|
+
this._openBtn = this.querySelector("[data-open-search]");
|
|
129
|
+
this._closeBtn = this.querySelector("[data-close-search]");
|
|
130
|
+
this._input = this.querySelector("[data-search-input]");
|
|
131
|
+
this._results = this.querySelector("[data-search-results]");
|
|
132
|
+
this._countWide = this.querySelector("[data-search-count]");
|
|
133
|
+
this._countNarrow = this.querySelector("[data-search-count-narrow]");
|
|
134
|
+
this._resultCountTemplate = this.dataset.resultCountTemplate || "{count} results";
|
|
135
|
+
// Snapshot the placeholder HTML before any search renders overwrite it.
|
|
136
|
+
this._placeholderHtml = this._results ? this._results.innerHTML : "";
|
|
137
|
+
|
|
138
|
+
// Platform keyboard-shortcut label — injected into [data-kbd-shortcut]
|
|
139
|
+
var nav = navigator;
|
|
140
|
+
var isMac = /Mac|iPhone|iPad|iPod/.test(
|
|
141
|
+
(nav.userAgentData && nav.userAgentData.platform) || nav.userAgent
|
|
142
|
+
);
|
|
143
|
+
this._shortcut = isMac ? "\\u2318K" : "Ctrl+K";
|
|
144
|
+
var kbdEl = this.querySelector("[data-kbd-shortcut]");
|
|
145
|
+
if (kbdEl) kbdEl.textContent = this._shortcut;
|
|
146
|
+
|
|
147
|
+
// Wire open/close handlers
|
|
148
|
+
var self = this;
|
|
149
|
+
if (this._openBtn) {
|
|
150
|
+
this._openBtn.addEventListener("click", function() { self.openDialog(); });
|
|
151
|
+
}
|
|
152
|
+
if (this._closeBtn) {
|
|
153
|
+
this._closeBtn.addEventListener("click", function() { self.closeDialog(); });
|
|
154
|
+
}
|
|
155
|
+
if (this._dialog) {
|
|
156
|
+
this._dialog.addEventListener("close", function() {
|
|
157
|
+
document.documentElement.style.overflow = "";
|
|
158
|
+
});
|
|
159
|
+
this._dialog.addEventListener("click", function(e) {
|
|
160
|
+
if (e.target === self._dialog) self.closeDialog();
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
if (this._input) {
|
|
164
|
+
this._input.addEventListener("input", function() { self.handleInput(); });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Global keyboard shortcut (⌘K / Ctrl+K to open)
|
|
168
|
+
this._keydownHandler = function(e) {
|
|
169
|
+
if ((e.metaKey || e.ctrlKey) && e.key === "k") {
|
|
170
|
+
e.preventDefault();
|
|
171
|
+
self.openDialog();
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
document.addEventListener("keydown", this._keydownHandler);
|
|
175
|
+
|
|
176
|
+
// View-Transitions compat: re-run on the v2 after-navigate event.
|
|
177
|
+
// Stored on the instance so disconnectedCallback can detach it on
|
|
178
|
+
// body swap when this element is NOT persisted via
|
|
179
|
+
// data-zfb-transition-persist (zudolab/zudo-doc#1523).
|
|
180
|
+
this._afterNavHandler = function() {
|
|
181
|
+
var kbdEl2 = self.querySelector("[data-kbd-shortcut]");
|
|
182
|
+
if (kbdEl2) kbdEl2.textContent = self._shortcut;
|
|
183
|
+
};
|
|
184
|
+
document.addEventListener(${JSON.stringify(AFTER_NAVIGATE_EVENT)}, this._afterNavHandler);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
disconnectedCallback() {
|
|
188
|
+
if (this._keydownHandler) {
|
|
189
|
+
document.removeEventListener("keydown", this._keydownHandler);
|
|
190
|
+
this._keydownHandler = null;
|
|
191
|
+
}
|
|
192
|
+
if (this._afterNavHandler) {
|
|
193
|
+
document.removeEventListener(${JSON.stringify(AFTER_NAVIGATE_EVENT)}, this._afterNavHandler);
|
|
194
|
+
this._afterNavHandler = null;
|
|
195
|
+
}
|
|
196
|
+
this.teardownSentinel();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
openDialog() {
|
|
200
|
+
if (!this._dialog) return;
|
|
201
|
+
document.documentElement.style.overflow = "hidden";
|
|
202
|
+
this._dialog.showModal();
|
|
203
|
+
if (this._input) {
|
|
204
|
+
this._input.focus();
|
|
205
|
+
this._input.select();
|
|
206
|
+
}
|
|
207
|
+
if (!this._entries && !this._loading) {
|
|
208
|
+
this.loadIndex();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
closeDialog() {
|
|
213
|
+
if (!this._dialog) return;
|
|
214
|
+
this._dialog.close();
|
|
215
|
+
document.documentElement.style.overflow = "";
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
handleInput() {
|
|
219
|
+
var self = this;
|
|
220
|
+
if (this._debounce) clearTimeout(this._debounce);
|
|
221
|
+
this._debounce = setTimeout(function() { self.search(); }, 150);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
loadIndex() {
|
|
225
|
+
if (this._loading) return;
|
|
226
|
+
this._loading = true;
|
|
227
|
+
var self = this;
|
|
228
|
+
var base = this.dataset.base || "/";
|
|
229
|
+
fetch(base + "search-index.json")
|
|
230
|
+
.then(function(r) {
|
|
231
|
+
if (!r.ok) throw new Error("HTTP " + r.status);
|
|
232
|
+
return r.json();
|
|
233
|
+
})
|
|
234
|
+
.then(function(data) {
|
|
235
|
+
self._entries = Array.isArray(data) ? data : (data.entries || []);
|
|
236
|
+
self._loading = false;
|
|
237
|
+
// If user already typed, search now
|
|
238
|
+
if (self._input && self._input.value.trim()) {
|
|
239
|
+
self.search();
|
|
240
|
+
}
|
|
241
|
+
})
|
|
242
|
+
.catch(function() {
|
|
243
|
+
self._loading = false;
|
|
244
|
+
// Index unavailable — silently degrade
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
search() {
|
|
249
|
+
var query = this._input ? this._input.value.trim() : "";
|
|
250
|
+
this._currentQuery = query;
|
|
251
|
+
|
|
252
|
+
if (!query) {
|
|
253
|
+
this.teardownSentinel();
|
|
254
|
+
this._allResults = [];
|
|
255
|
+
this._shownCount = 0;
|
|
256
|
+
if (this._results) this._results.innerHTML = this.placeholderHtml();
|
|
257
|
+
this.updateCount();
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (!this._entries) {
|
|
262
|
+
if (this._results) {
|
|
263
|
+
this._results.innerHTML = "<p class=\\"text-small text-muted\\">Loading search index\\u2026</p>";
|
|
264
|
+
}
|
|
265
|
+
if (!this._loading) this.loadIndex();
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
var terms = parseTerms(query);
|
|
270
|
+
var scored = [];
|
|
271
|
+
for (var i = 0; i < this._entries.length; i++) {
|
|
272
|
+
var s = scoreEntry(this._entries[i], terms);
|
|
273
|
+
if (s > 0) scored.push({ entry: this._entries[i], score: s });
|
|
274
|
+
}
|
|
275
|
+
scored.sort(function(a, b) { return b.score - a.score; });
|
|
276
|
+
this._allResults = scored;
|
|
277
|
+
this._shownCount = 0;
|
|
278
|
+
this.teardownSentinel();
|
|
279
|
+
this.updateCount();
|
|
280
|
+
|
|
281
|
+
if (!scored.length) {
|
|
282
|
+
if (this._results) {
|
|
283
|
+
this._results.innerHTML = "<p class=\\"text-small text-muted\\">No results found.</p>";
|
|
284
|
+
}
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (this._results) this._results.innerHTML = "";
|
|
289
|
+
this.loadMore();
|
|
290
|
+
if (this._shownCount < this._allResults.length) {
|
|
291
|
+
this.setupSentinel();
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
loadMore() {
|
|
296
|
+
if (this._isLoadingBatch) return;
|
|
297
|
+
if (this._shownCount >= this._allResults.length) return;
|
|
298
|
+
this._isLoadingBatch = true;
|
|
299
|
+
try {
|
|
300
|
+
var batch = this._allResults.slice(this._shownCount, this._shownCount + PAGE_SIZE);
|
|
301
|
+
var self = this;
|
|
302
|
+
for (var i = 0; i < batch.length; i++) {
|
|
303
|
+
var article = self.renderResult(batch[i].entry);
|
|
304
|
+
if (self._sentinel && self._sentinel.parentNode === self._results) {
|
|
305
|
+
self._results.insertBefore(article, self._sentinel);
|
|
306
|
+
} else if (self._results) {
|
|
307
|
+
self._results.appendChild(article);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
this._shownCount += batch.length;
|
|
311
|
+
if (this._shownCount >= this._allResults.length) {
|
|
312
|
+
this.teardownSentinel();
|
|
313
|
+
}
|
|
314
|
+
} finally {
|
|
315
|
+
this._isLoadingBatch = false;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
setupSentinel() {
|
|
320
|
+
this.teardownSentinel();
|
|
321
|
+
if (!this._results) return;
|
|
322
|
+
var sentinel = document.createElement("div");
|
|
323
|
+
sentinel.setAttribute("data-search-sentinel", "");
|
|
324
|
+
sentinel.setAttribute("aria-hidden", "true");
|
|
325
|
+
sentinel.style.height = "1px";
|
|
326
|
+
sentinel.style.width = "100%";
|
|
327
|
+
this._results.appendChild(sentinel);
|
|
328
|
+
this._sentinel = sentinel;
|
|
329
|
+
var self = this;
|
|
330
|
+
this._observer = new IntersectionObserver(function(entries) {
|
|
331
|
+
for (var i = 0; i < entries.length; i++) {
|
|
332
|
+
if (entries[i].isIntersecting) self.loadMore();
|
|
333
|
+
}
|
|
334
|
+
}, { root: this._results, rootMargin: "200px 0px" });
|
|
335
|
+
this._observer.observe(sentinel);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
teardownSentinel() {
|
|
339
|
+
if (this._observer) { this._observer.disconnect(); this._observer = null; }
|
|
340
|
+
if (this._sentinel) { this._sentinel.remove(); this._sentinel = null; }
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
updateCount() {
|
|
344
|
+
var count = this._allResults.length;
|
|
345
|
+
var template = this._resultCountTemplate;
|
|
346
|
+
var text = count > 0 ? template.replace("{count}", String(count)) : "";
|
|
347
|
+
var show = !!text;
|
|
348
|
+
if (this._countWide) {
|
|
349
|
+
this._countWide.textContent = text;
|
|
350
|
+
this._countWide.classList.toggle("hidden", !show);
|
|
351
|
+
}
|
|
352
|
+
if (this._countNarrow) {
|
|
353
|
+
this._countNarrow.textContent = text;
|
|
354
|
+
this._countNarrow.classList.toggle("hidden", !show);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
placeholderHtml() {
|
|
359
|
+
return this._placeholderHtml;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
renderResult(entry) {
|
|
363
|
+
var article = document.createElement("article");
|
|
364
|
+
article.className = "-mx-hsp-lg border-b border-muted";
|
|
365
|
+
var link = document.createElement("a");
|
|
366
|
+
link.href = entry.url || "#";
|
|
367
|
+
link.className =
|
|
368
|
+
"group block px-hsp-lg py-vsp-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent";
|
|
369
|
+
var title = document.createElement("span");
|
|
370
|
+
title.className =
|
|
371
|
+
"font-semibold text-fg group-hover:text-accent group-hover:underline group-focus-visible:underline";
|
|
372
|
+
var terms = parseTerms(this._currentQuery);
|
|
373
|
+
title.innerHTML = highlightTerms(entry.title || "", terms);
|
|
374
|
+
link.appendChild(title);
|
|
375
|
+
var text = entry.description || entry.body;
|
|
376
|
+
if (text) {
|
|
377
|
+
var excerpt = document.createElement("p");
|
|
378
|
+
excerpt.className =
|
|
379
|
+
"mt-vsp-2xs text-caption text-muted leading-relaxed group-hover:underline group-focus-visible:underline decoration-muted";
|
|
380
|
+
var truncated = truncate(text, this._currentQuery, 200);
|
|
381
|
+
excerpt.innerHTML = highlightTerms(truncated, terms);
|
|
382
|
+
link.appendChild(excerpt);
|
|
383
|
+
}
|
|
384
|
+
article.appendChild(link);
|
|
385
|
+
return article;
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
})();`;
|