feat(linkedin-studio): N3.5 — MR-F8 build-html-forsoning (blockquote+lenker+FIGUR->SVG port + paritetstest) [skip-docs]

Plugin eier hele render/ (KTG-go 16.07, eierskapsvalg a). Portet fra
maskinrommets tools/build-html.mjs (read-only): flerlinjers blockquote
(avsnitt i quote), [tekst](url)-lenker med http/https/mailto-whitelist,
FIGUR-markoer -> inline SVG med figcaption + fallback til blockquote.
CSS for de tre konstruktene. 7 nye paritetstester (repoets testtilfeller
kopiert + med-SVG-case); paritetsbevis: samme fixture gjennom begge
motorer = byte-identisk parser-HTML. Render-floor 53 -> 60.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: c61b308e-e4fa-4a49-9ff0-a3ce725cf703
This commit is contained in:
Kjell Tore Guttormsen 2026-07-17 03:22:20 +02:00
commit 81510297db
3 changed files with 145 additions and 7 deletions

View file

@ -42,13 +42,19 @@ function esc(s) {
}
// ---------------------------------------------------------------------------
// Inline markdown: `kode`, **fet**, *kursiv*. «» og — beholdes uendret.
// Inline markdown: `kode`, [lenke](url), **fet**, *kursiv*. «» og — beholdes.
// Tar uescapet tekst, returnerer escaped HTML med inline-tagger.
// ---------------------------------------------------------------------------
export function inline(text) {
let out = esc(text);
// `kode` først, slik at * og ** inni en kode-span ikke tolkes som fet/kursiv
out = out.replace(/`([^`]+)`/g, (_, c) => `<code>${c}</code>`);
// [tekst](url) — kun http(s)/mailto får href; andre skjemaer beholder teksten
out = out.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_, label, url) =>
/^(https?:|mailto:)/i.test(url)
? `<a href="${url}" target="_blank" rel="noopener">${label}</a>`
: label
);
// **fet** før *kursiv* for å unngå konflikt
out = out.replace(/\*\*([^*]+)\*\*/g, (_, c) => `<strong>${c}</strong>`);
out = out.replace(/\*([^*]+)\*/g, (_, c) => `<em>${c}</em>`);
@ -118,10 +124,25 @@ function isTableLine(t) {
// ---------------------------------------------------------------------------
// Kompakt markdown -> HTML for body.
// Håndterer: # .. #### overskrifter, | tabeller |, - punktlister,
// 1. nummererte lister, > blockquote, --- horisontal linje, `kode`, og
// avsnitt (blanklinje-separert).
// 1. nummererte lister, > blockquote (flerlinjers, med FIGUR-markør),
// [lenker](url), --- horisontal linje, `kode`, og avsnitt (blanklinje-separert).
// Første avsnitt får drop-cap-klasse. Avsnitt etter det første: .indent.
// ---------------------------------------------------------------------------
// Figur-markør: les figurer/figN*.svg fra serie-mappa (process.cwd()).
// Returnerer rå SVG-streng, eller null hvis ingen matchende fil finnes.
function readFigureSvg(n) {
const figDir = path.join(process.cwd(), "figurer");
if (!fs.existsSync(figDir)) return null;
const re = new RegExp("^fig" + n + "(?:[-.].*)?\\.svg$", "i");
const match = fs.readdirSync(figDir).filter((f) => re.test(f)).sort()[0];
if (!match) return null;
return fs
.readFileSync(path.join(figDir, match), "utf8")
.replace(/<\?xml[^>]*\?>\s*/i, "")
.trim();
}
export function markdownToHtml(body) {
const lines = body.replace(/\r\n/g, "\n").split("\n");
const blocks = [];
@ -167,14 +188,41 @@ export function markdownToHtml(body) {
continue;
}
// Blockquote (sammenhengende > -linjer)
// Blockquote (sammenhengende > -linjer; tom > -linje = nytt avsnitt i samme quote)
if (/^>\s?/.test(trimmed)) {
const qbuf = [];
const qlines = [];
while (i < lines.length && /^>\s?/.test(lines[i].trim())) {
qbuf.push(lines[i].trim().replace(/^>\s?/, ""));
qlines.push(lines[i].trim().replace(/^>\s?/, ""));
i++;
}
blocks.push(`<blockquote><p>${inline(qbuf.join(" ").trim())}</p></blockquote>`);
const paras = [];
let cur = [];
for (const ql of qlines) {
if (ql.trim() === "") {
if (cur.length) { paras.push(cur.join(" ").trim()); cur = []; }
} else cur.push(ql);
}
if (cur.length) paras.push(cur.join(" ").trim());
// Figur-markør: blockquote som starter med **[FIGUR N — ...]**.
// Finnes figurer/figN*.svg → inline SVG (rendres i nettleser).
// Ellers: vis spec'en som vanlig blockquote (uendret oppførsel).
const figm = paras.length && paras[0].match(/^\*\*\[FIGUR\s+(\d+)\b/);
if (figm) {
const svg = readFigureSvg(figm[1]);
if (svg) {
const capm = paras[0].match(/«([^»]+)»/);
const caption = capm ? `Figur ${figm[1]}${capm[1]}` : "";
blocks.push(
`<figure class="fig">${svg}` +
(caption ? `<figcaption>${inline(caption)}</figcaption>` : "") +
"</figure>"
);
continue;
}
}
blocks.push("<blockquote>" + paras.map((p) => `<p>${inline(p)}</p>`).join("") + "</blockquote>");
continue;
}
@ -337,6 +385,7 @@ h1.title {
.body h2 { font-size: 1.5rem; font-weight: 700; margin: 2rem 0 0.6rem; line-height: 1.2; }
.body h3 { font-size: 1.2rem; font-weight: 700; margin: 1.6rem 0 0.5rem; line-height: 1.25; }
.body h4 { font-size: 1.05rem; font-weight: 700; margin: 1.3rem 0 0.4rem; line-height: 1.3; }
.body a { color: var(--accent); text-decoration: underline; text-underline-offset: 2px; }
.body code {
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 0.86em;
@ -367,6 +416,23 @@ h1.title {
font-style: italic;
color: #333;
}
.body blockquote p { margin: 0 0 0.7em; }
.body blockquote p:last-child { margin-bottom: 0; }
.body figure.fig { margin: 1.9rem 0; text-align: center; }
.body figure.fig svg {
max-width: 100%;
height: auto;
border: 1px solid var(--rule);
background: #fff;
border-radius: 4px;
}
.body figure.fig figcaption {
font-family: var(--sans);
font-size: 0.8rem;
color: var(--muted);
font-style: italic;
margin-top: 0.5rem;
}
.body hr {
border: 0;
border-top: 1px solid var(--rule);