diff --git a/CLAUDE.md b/CLAUDE.md
index 91c17fe..3b3b074 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## What This Is
-Build-time aggregator that downloads pre-built static doc sites from GitHub Release artifacts, injects a persistent branded chrome (56px top nav), and produces a single static deployment. Served by nginx in Docker. Can also run as a web-fragment inside an Angular SSR gateway.
+Build-time aggregator that downloads pre-built static doc sites from GitHub Release artifacts, wraps them in a persistent branded masthead, and produces a single static deployment. Served by nginx in Docker. Can also run as a web-fragment inside an Angular SSR gateway.
## Commands
@@ -44,7 +44,8 @@ apps.json (registry)
→ [1] Fetch Release artifacts OR build local repos → apps/{slug}/
→ [2] Copy non-HTML assets → public/{slug}/ (Astro serves as static)
→ [3] astro build: [...path].astro enumerates all HTML via getStaticPaths
- → transformSubAppHtml() rewrites URLs + injects chrome
+ → transformSubAppHtml() rewrites URLs + splits the document,
+ which Base.astro then re-hosts (masthead + head/body)
→ dist/
```
@@ -59,34 +60,43 @@ Orchestrator: `scripts/build-vite.js`. Flags: `--local`, `--headless`, `--path-p
### Core Data Flow
-`apps.json` → `scripts/fetch-apps.js` downloads `dist.tar.gz` per app → `apps/{slug}/` → Astro's `src/pages/[...path].astro` catchall uses `getStaticPaths()` from `src/utils/apps.js` to enumerate every HTML file → `src/utils/transform.js` rewrites URLs and injects chrome → static output in `dist/`.
+`apps.json` → `scripts/fetch-apps.js` downloads `dist.tar.gz` per app → `apps/{slug}/` → Astro's `src/pages/[...path].astro` catchall uses `getStaticPaths()` from `src/utils/apps.js` to enumerate every HTML file → `src/utils/transform.js` rewrites URLs and splits the document → `Base.astro` re-hosts the parts → static output in `dist/`.
### Key Source Files
-- `src/pages/[...path].astro` — Catchall route, renders every sub-app page. Injects `` for Astro client-side transitions.
+- `src/pages/[...path].astro` — Catchall route, renders every sub-app page through `Base.astro`
- `src/pages/index.astro` — Landing catalog page
- `src/utils/apps.js` — `getAppPages()` enumerates sub-app HTML (manifest-driven or filesystem crawl)
-- `src/utils/transform.js` — `transformSubAppHtml()`: URL rewriting, chrome injection, headless transforms
-- `src/templates/chrome.js` — Chrome HTML template + client-side SPA router script
+- `src/utils/transform.js` — `transformSubAppHtml()`: URL rewriting, document splitting (head/body/title/body-class), headless transforms
+- `src/layouts/Base.astro` — The one document shell: head, fonts, marketplace CSS, theme script, ``
+- `src/components/Chrome.astro` — 56px fixed top bar (standalone mode only)
+- `src/components/Masthead.astro` — Persistent Knowledge base header + Library/current-app sub-nav (all pages, both modes)
+- `src/templates/chrome.js` — Inline theme script + shadow-DOM compat styles injected by the layout
- `scripts/build-vite.js` — Build orchestrator (3-step pipeline)
- `scripts/fetch-apps.js` — GitHub Release artifact downloader
### Two Modes
-**Non-headless** (standalone): Injects chrome bar, SPA router, theme toggle. Client-side router intercepts navigation, fetches HTML, swaps DOM content.
+Both modes render the same document: the masthead (`Masthead.astro`) — branding plus the Library / current-app sub-navigation — on every page, and nothing else chrome-like. There is no fixed top bar and no app switcher; the masthead is the navigation.
-**Headless** (web-fragment): Strips dark mode class, marks `data-mp-headless="true"`, injects shadow DOM compat styles. No chrome bar. Designed for embedding in a web-fragments gateway.
+**Non-headless** (standalone): Plain marketplace pages. Navigation is Astro's `` (view transitions).
+
+**Headless** (web-fragment): Marks `data-mp-headless="true"` and injects shadow DOM compat styles. Designed for embedding in a web-fragments gateway.
+
+### Light Only
+
+The marketplace has no dark mode: no theme toggle, no persisted theme, no `dark` class, no dark palette. `transformSubAppHtml()` strips any sub-app theme bootstrap and `dark` body class so an embedding host's theme cannot bleed into the fragment.
### URL Rewriting
-`transform.js` rewrites all relative and root-relative URLs to absolute `/{prefix}/{slug}/...` paths. Runs before chrome injection to prevent double-rewriting. Removes `` tags.
+`transform.js` rewrites all relative and root-relative URLs to absolute `/{prefix}/{slug}/...` paths. Runs before the document is split so nothing is double-rewritten. Removes `` tags.
## Contract for Doc Apps
Apps registered in `apps.json` must comply with:
- `contract/schema.json` — JSON Schema for `marketplace.json` manifest
- `contract/HEADLESS_RULES.md` — Structural requirements (headless HTML, relative paths, `data-mp-headless` attribute)
-- `contract/STYLE_GUIDE.md` — Design tokens, typography, dark mode support
+- `contract/STYLE_GUIDE.md` — Design tokens and typography (light only — the marketplace has no dark mode)
## Testing
diff --git a/README.md b/README.md
index db9ac16..4b7eb8a 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
# knowledge-base
> A unified documentation portal — wraps independently-maintained doc apps into a
-> single deployable with a persistent branded chrome, and can be embedded as a
+> single deployable with a persistent branded masthead, and can be embedded as a
> web fragment inside a host app.
---
@@ -279,12 +279,12 @@ knowledge-base/
│ │ ├── index.astro ← Landing catalog
│ │ └── [...path].astro ← Catch-all: renders every sub-app page
│ ├── layouts/Base.astro
-│ ├── components/ ← AppCard, AppIcon, Chrome
-│ ├── templates/chrome.js ← Chrome HTML + client SPA router
+│ ├── components/ ← AppCard, AppIcon, Masthead
+│ ├── templates/shadow-compat.js ← Shadow-DOM design-token styles
│ ├── styles/marketplace.css ← Design tokens + Tailwind
│ └── utils/
│ ├── apps.js ← getAppPages() page enumeration
-│ └── transform.js ← URL rewriting + chrome/headless injection
+│ └── transform.js ← URL rewriting + sub-app document splitting
├── scripts/
│ ├── build-vite.js ← Build orchestrator
│ ├── fetch-apps.js ← GitHub Release download + extract
diff --git a/contract/HEADLESS_RULES.md b/contract/HEADLESS_RULES.md
index 201469f..ddd4831 100644
--- a/contract/HEADLESS_RULES.md
+++ b/contract/HEADLESS_RULES.md
@@ -128,8 +128,9 @@ At minimum the following CSS custom properties must be present:
--text-heading, --text-body, --text-muted
```
-Copy the `@theme` block and `:root` / `.dark` variable declarations from
-[`src/input.css`](../src/input.css) in this repository.
+Copy the `@theme` block and `:root` variable declarations from
+[`src/styles/marketplace.css`](../src/styles/marketplace.css) in this repository.
+There is deliberately no `.dark` set — the marketplace is light-only.
---
diff --git a/contract/STYLE_GUIDE.md b/contract/STYLE_GUIDE.md
index 5745e62..3fc1d18 100644
--- a/contract/STYLE_GUIDE.md
+++ b/contract/STYLE_GUIDE.md
@@ -52,23 +52,16 @@ Ensure Inter is loaded from Google Fonts or bundled:
## Dark mode
-Dark mode is **class-based**: add `class="dark"` to `` (default in standalone builds).
-The marketplace propagates the user's theme preference to all embedded apps.
+**There is none.** The marketplace is light-only: no theme toggle, no persisted
+preference, no dark palette.
-Required dark-mode variable overrides (copy from `src/input.css`):
+Do not ship a dark-mode bootstrap. When an app is integrated, the build strips any
+`localStorage`-based theme script and removes a `dark` class from `
`, so a
+dark theme would be dropped at integration time anyway — and inside a web fragment
+it would clash with the host application's own theme.
-```css
-.dark {
- --bg-page: #0a0d14;
- --bg-card: #111827;
- --bg-strong: #1f2937;
- --bg-subtle: #1a2236;
- --border: #1f2937;
- --text-heading: #ffffff;
- --text-body: #d1d5db;
- --text-muted: #9ca3af;
-}
-```
+An app may still define `.dark` styles for its **standalone** deployment; they will
+simply never activate inside the marketplace.
---
@@ -77,9 +70,8 @@ Required dark-mode variable overrides (copy from `src/input.css`):
- Fixed left sidebar, **64px wide** (`w-64`)
- Background: `var(--bg-card)`
- Border right: `1px solid var(--border)`
-- `id="sidebar"` required for marketplace chrome injection
-- In **headless** mode: `top: 0` (marketplace chrome handles the offset)
-- In **standalone** mode: `top: 56px` (offset by site header)
+- `id="sidebar"` required so the marketplace can position it
+- `top: 0` — the marketplace renders no fixed top bar, so nothing needs offsetting
---
@@ -151,7 +143,7 @@ padding: 0.75rem 1rem;
- [ ] Only brand color tokens used (no raw hex brand colors)
- [ ] Inter font loaded
-- [ ] Both light and dark CSS variable sets defined
+- [ ] Light CSS variable set defined (no dark set — the marketplace is light-only)
- [ ] `id="sidebar"` present on the left navigation
- [ ] `id="content"` or `` wraps the prose area
- [ ] No inline styles that override design tokens with hardcoded values
diff --git a/src/components/Chrome.astro b/src/components/Chrome.astro
deleted file mode 100644
index 9ee81ce..0000000
--- a/src/components/Chrome.astro
+++ /dev/null
@@ -1,97 +0,0 @@
----
-// src/components/Chrome.astro
-import AppIcon from './AppIcon.astro';
-
-interface App {
- slug: string;
- name: string;
- icon?: string;
-}
-
-interface Props {
- apps: App[];
- activeSlug?: string;
- base?: string;
-}
-
-const { apps, activeSlug = '', base = '/knowledge-base' } = Astro.props;
-const activeApp = apps.find(a => a.slug === activeSlug);
-
-const link = (path: string) => `${base}/${path}`;
----
-
-
diff --git a/src/components/Masthead.astro b/src/components/Masthead.astro
new file mode 100644
index 0000000..5c2af1f
--- /dev/null
+++ b/src/components/Masthead.astro
@@ -0,0 +1,81 @@
+---
+// src/components/Masthead.astro
+//
+// Persistent Knowledge base masthead. Rendered by every page through Base.astro
+// (landing catalog, packaged sub-app pages, iframe entries) in both headless and
+// standalone builds, so the branding and the way back to the catalog never
+// disappear once a user opens a documentation app.
+//
+// Sub-navigation:
+// • "Library" → the catalog of all documentation sites
+// • a dynamic second entry naming the app currently being viewed
+// The entry matching the current location is inert text with aria-current="page"
+// rather than a link to itself.
+import AppIcon from './AppIcon.astro';
+
+interface App {
+ slug: string;
+ name: string;
+ icon?: string;
+}
+
+interface Props {
+ apps: App[];
+ activeSlug?: string;
+ base?: string;
+ /** True when the current page is the active app's own index (its crumb is then inert). */
+ appRoot?: boolean;
+}
+
+const { apps, activeSlug = '', base = '/knowledge-base', appRoot = false } = Astro.props;
+
+const activeApp = apps.find(a => a.slug === activeSlug);
+const onLibrary = !activeApp;
+---
+
+
+
+
+
+
+
+ Docs
+
+
+ {/* On the catalog the masthead IS the page heading; on a documentation page the
+ sub-app supplies its own
, so the masthead title stays non-heading text. */}
+ {onLibrary
+ ?
Knowledge base
+ :
Knowledge base
+ }
+
+
+ Browse and access all documentation sites. Each app is independently
+ maintained and deployed — one consistent experience.
+
+
+
+
+
diff --git a/src/layouts/Base.astro b/src/layouts/Base.astro
index fb1af71..756077d 100644
--- a/src/layouts/Base.astro
+++ b/src/layouts/Base.astro
@@ -1,23 +1,40 @@
---
// src/layouts/Base.astro
+//
+// The single document shell for every marketplace page — landing catalog,
+// packaged sub-app pages and iframe entries alike. It owns , the fonts,
+// the marketplace stylesheet, and the shadow-DOM compat styles,
+// so none of that has to be string-injected into sub-app documents any more.
+//
+// The marketplace is light-only. There is no theme toggle, no persisted theme and
+// no `dark` class anywhere, so an embedding host's theme can never bleed in.
import '../styles/marketplace.css';
import { ClientRouter } from 'astro:transitions';
-import { shadowCompatStyle } from '../templates/chrome.js';
+import { shadowCompatStyle } from '../templates/shadow-compat.js';
interface Props {
title?: string;
headless?: boolean;
+ /** Extra classes for — packaged sub-app pages pass their own body class. */
bodyClass?: string;
+ /** Full-height flex column body. Off for sub-app content, which brings its own layout. */
+ flexBody?: boolean;
}
const {
title = 'Knowledge base',
headless = false,
bodyClass = '',
+ flexBody = true,
} = Astro.props;
const HEADLESS = process.env.MP_HEADLESS !== 'false';
const effectiveHeadless = headless || HEADLESS;
+
+const bodyClasses = [
+ flexBody ? 'min-h-screen flex flex-col' : '',
+ bodyClass,
+].filter(Boolean).join(' ') || undefined;
---
@@ -27,14 +44,19 @@ const effectiveHeadless = headless || HEADLESS;
{title}
+ {/* Sub-app head contents (their stylesheets, meta and inline scripts) are slotted
+ in first so the marketplace stylesheet — injected by Astro from the CSS import
+ above — still wins on equal specificity, exactly as it did when the old string
+ transform appended it to the sub-app document. */}
+
-
+ {/* data-astro-transition-persist keeps the font stylesheet stable across
+ ClientRouter navigation so it isn't churned on every page swap — avoids
+ web-fragments #297 accumulation. */}
-
+
diff --git a/src/pages/[...path].astro b/src/pages/[...path].astro
index 9925e92..54254db 100644
--- a/src/pages/[...path].astro
+++ b/src/pages/[...path].astro
@@ -4,13 +4,17 @@
// Catchall route that serves every sub-app page as a static Astro page.
// getStaticPaths enumerates all HTML files under apps/{slug}/ for every packaged
// entry in apps.json, plus a single route per iframe-type entry (issue #10).
+//
+// Packaged pages are not emitted verbatim: transformSubAppHtml() rewrites their
+// URLs and splits the document, and Base.astro re-hosts the parts. That keeps the
+// masthead, fonts, marketplace CSS and ClientRouter in one place for all page
+// types (issue #24).
import { readFileSync } from 'node:fs';
-import { ClientRouter } from 'astro:transitions';
import { getAppPages } from '../utils/apps.js';
import { transformSubAppHtml } from '../utils/transform.js';
import Base from '../layouts/Base.astro';
-import Chrome from '../components/Chrome.astro';
+import Masthead from '../components/Masthead.astro';
const PREFIX = 'knowledge-base';
@@ -25,36 +29,39 @@ export function getStaticPaths() {
const props = Astro.props;
const { slug, appHeadless, apps, title } = props;
-// Packaged apps: read the sub-app HTML file and transform it. Split at
-// so is injected for Astro's client-side transitions.
-let beforeHeadEnd = '';
-let afterHeadEnd = '';
-if (!props.iframe) {
- const raw = readFileSync(props.file, 'utf-8');
- const html = transformSubAppHtml(raw, apps, slug, props.fileRelDir, PREFIX, appHeadless);
- const headEnd = html.indexOf('');
- beforeHeadEnd = headEnd >= 0 ? html.slice(0, headEnd) : html;
- afterHeadEnd = headEnd >= 0 ? html.slice(headEnd) : '';
-}
+const parts = props.iframe
+ ? null
+ : transformSubAppHtml(readFileSync(props.file, 'utf-8'), slug, props.fileRelDir, PREFIX);
+
+const app = apps.find((a: any) => a.slug === slug);
+const pageTitle = title ?? parts?.title ?? app?.name ?? 'Knowledge base';
-// iFrame entries: fill the viewport (below the 56px chrome when standalone; full
-// height when headless/embedded, where the host supplies its own chrome).
-const iframeHeight = appHeadless ? '100vh' : 'calc(100vh - var(--mp-chrome-h, 56px))';
+// The app's own crumb in the masthead is inert on the app index and a link deeper in.
+const atAppRoot = !props.fileRelDir;
---
-{props.iframe ? (
-
- {!appHeadless && }
-
+
+
+ {parts && }
+
+
+
+ {props.iframe ? (
+
-
-) : (
-
-)}
+ ) : (
+
+ )}
+
diff --git a/src/pages/index.astro b/src/pages/index.astro
index effa02e..b5c1120 100644
--- a/src/pages/index.astro
+++ b/src/pages/index.astro
@@ -3,7 +3,7 @@
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import Base from '../layouts/Base.astro';
-import Chrome from '../components/Chrome.astro';
+import Masthead from '../components/Masthead.astro';
import AppCard from '../components/AppCard.astro';
const HEADLESS = process.env.MP_HEADLESS !== 'false';
@@ -16,31 +16,9 @@ const buildDate = new Date().toISOString().slice(0, 10);
---
- {!HEADLESS && }
+
-
-
-
-
-
-
-
- Docs
-
-
- Knowledge base
-
-
- Browse and access all documentation sites. Each app is independently
- maintained and deployed — one consistent experience.
-
-
-
-
diff --git a/src/styles/marketplace.css b/src/styles/marketplace.css
index fb91551..d6da730 100644
--- a/src/styles/marketplace.css
+++ b/src/styles/marketplace.css
@@ -4,9 +4,6 @@
@source "../src/templates/**/*.js";
@source "../scripts/**/*.js";
-/* ── Class-based dark mode — also covers wf-html.dark used by web-fragments ── */
-@custom-variant dark (&:where(.dark, .dark *));
-
/* ── Design tokens ──────────────────────────────────────────────────────── */
@theme {
/* Brand palette */
@@ -23,12 +20,13 @@
--font-mono: 'SF Mono', 'Fira Code', 'Fira Mono', ui-monospace, monospace;
}
-/* ── Semantic CSS variables (light & dark) ─────────────────────────────── */
-/* Note: @tailwindcss/vite compiles :root to :root,:host automatically, so
+/* ── Semantic CSS variables ─────────────────────────────────────────────── */
+/* The marketplace is light-only — there is deliberately no dark palette here,
+ so an embedding host's dark theme cannot bleed into the fragment.
+ Note: @tailwindcss/vite compiles :root to :root,:host automatically, so
custom properties are available inside shadow DOMs via :host inheritance.
- Shadow DOM element-level declarations (wf-html, wf-document) and the
- :host(.dark) dark-mode override are injected as an inline `.trim();
-
-// ── Helpers ───────────────────────────────────────────────────────────────────
-
-function escHtml(str) {
- return String(str ?? '')
- .replace(/&/g, '&')
- .replace(//g, '>')
- .replace(/"/g, '"');
-}
-
-export function appIconSvg(icon, size = 18) {
- const s = size;
- const icons = {
- 'book-open': ``,
- 'cube': ``,
- 'chip': ``,
- 'chart-bar': ``,
- 'shield': ``,
- 'cog': ``,
- 'terminal': ``,
- 'globe': ``,
- 'layers': ``,
- 'lightning-bolt':``,
- 'document': ``,
- 'collection': ``,
- 'puzzle': ``,
- 'database': ``,
- };
- return icons[icon] ?? icons['book-open'];
-}
diff --git a/src/templates/shadow-compat.js b/src/templates/shadow-compat.js
new file mode 100644
index 0000000..b5c5c07
--- /dev/null
+++ b/src/templates/shadow-compat.js
@@ -0,0 +1,22 @@
+// src/templates/shadow-compat.js
+//
+// Inline `.trim();
diff --git a/src/utils/transform.js b/src/utils/transform.js
index 1da34db..95db714 100644
--- a/src/utils/transform.js
+++ b/src/utils/transform.js
@@ -1,8 +1,12 @@
// src/utils/transform.js
//
// Pure sub-app HTML transformation utilities for the Astro catchall page.
-
-import { chromeHtml, chromeScript, marketplaceRouterScript, shadowCompatStyle } from '../templates/chrome.js';
+//
+// Packaged sub-app pages arrive as complete pre-built documents. Rather than
+// emitting them verbatim, the catchall route renders them through Base.astro so
+// the layout owns the masthead, fonts, marketplace CSS and for
+// every page. This module rewrites the sub-app URLs and splits the document into
+// the parts the layout needs.
// ── URL rewriting ─────────────────────────────────────────────────────────────
@@ -30,8 +34,6 @@ function resolveUrl(url, base, prefix, slug) {
/**
* Rewrites all href/src/action URLs in the HTML to absolute paths.
- * Runs BEFORE any marketplace assets are injected so injected absolute
- * URLs (e.g. /__wf/knowledge-base/style.css) are never double-rewritten.
* Removes any pre-existing tag.
*
* @param {string} html
@@ -68,37 +70,15 @@ function rewriteUrlsToAbsolute(html, prefix, slug, fileRelDir) {
return html;
}
-// ── Sub-app HTML transformation ───────────────────────────────────────────────
-
-const FONT_PRECONNECT = `
- `;
-
/**
- * Transforms raw sub-app HTML for embedding in the marketplace.
- *
- * Steps (all applied in order):
- * 1. Rewrite all relative/root-relative URLs to absolute /knowledge-base/slug/… paths
- * 2. Add data-astro-transition-persist to all stylesheet links (prevents FOUC)
- * 3. Inject marketplace CSS link (via /__wf/ gateway route)
- * 4. Inject Google Fonts preconnect if Inter is not already loaded
- * 5a. Headless: strip dark-mode, mark document, inject shadow DOM compat styles
- * 5b. Non-headless: inject chrome bar and SPA router
- *
- * @param {string} html
- * @param {Array} apps - full apps.json array (for chrome nav)
- * @param {string} slug - this app's slug
- * @param {string} fileRelDir - path of this HTML file relative to the app root
- * @param {string} prefix - URL prefix, e.g. 'knowledge-base'
- * @param {boolean} headless - whether to render in headless (fragment) mode
+ * Adds data-astro-transition-persist to every stylesheet so Astro's
+ * ClientRouter keeps them in place across page transitions instead of removing
+ * and re-adding them. Inside web-fragments, reframed relocates head nodes out of
+ * wf-head, so Astro's cleanup can no longer find them and they accumulate
+ * unbounded — see web-fragments issue #297.
*/
-export function transformSubAppHtml(html, apps, slug, fileRelDir, prefix, headless) {
- // 1. Rewrite all URLs to absolute /knowledge-base/slug/... (must be first)
- html = rewriteUrlsToAbsolute(html, prefix, slug, fileRelDir);
-
- // 2. Add data-astro-transition-persist to every stylesheet so that
- // Astro's ClientRouter keeps them in place across page transitions (no FOUC).
- // Strip any trailing self-closing slash from attrs before re-serialising.
- html = html.replace(
+function persistStylesheets(html) {
+ return html.replace(
/]*rel\s*=\s*["']stylesheet["'][^>]*)>/gi,
(match, attrs) => {
if (attrs.includes('data-astro-transition-persist')) return match;
@@ -109,65 +89,65 @@ export function transformSubAppHtml(html, apps, slug, fileRelDir, prefix, headle
return ``;
},
);
+}
- // 3. Inject marketplace CSS via /__wf/ gateway route
- const stylePath = `/__wf/${prefix}/style.css`;
- if (!html.includes(`href="${stylePath}"`)) {
- html = html.replace(/<\/head>/i, ` \n`);
- }
-
- // 4. Inject font preconnect if Inter isn't loaded
- if (!html.includes('fonts.googleapis.com')) {
- const fontsPersist = FONT_PRECONNECT.replace(
- /rel="stylesheet"/,
- 'rel="stylesheet" data-astro-transition-persist="css-google-fonts-inter"',
- );
- html = html.replace(/<\/head>/i, ` ${fontsPersist}\n`);
- }
-
- if (headless) {
- // H-a. Force light mode: remove `dark` class and localStorage theme script
- html = html.replace(
- /(]*)\bclass=(["'])([^"']*)\2([^>]*>)/i,
- (_, pre, q, cls, post) => {
- const cleaned = cls.replace(/\bdark\b/g, '').replace(/\s+/g, ' ').trim();
- const classAttr = cleaned ? ` class=${q}${cleaned}${q}` : '';
- return `${pre.trimEnd()}${classAttr}${post}`;
- },
- );
- html = html.replace(/