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:
parent
1a67bd2cb8
commit
81510297db
3 changed files with 145 additions and 7 deletions
|
|
@ -1,7 +1,11 @@
|
|||
import { describe, test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { markdownToHtml, inline } from '../build-html.mjs';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
describe('markdownToHtml — tables (beslutning H)', () => {
|
||||
test('converts a pipe table to <table>/<tr>/<td>', () => {
|
||||
const md = [
|
||||
|
|
@ -63,3 +67,66 @@ describe('inline — backtick code span (beslutning H)', () => {
|
|||
assert.match(inline('**fet** og *kursiv*'), /<em>kursiv<\/em>/);
|
||||
});
|
||||
});
|
||||
|
||||
// Paritetstilfeller portet fra maskinrommets tools/__tests__/build-html.test.mjs
|
||||
// (N3.5/MR-F8): lenker, flerlinjers blockquote, FIGUR→SVG. Samme markdown skal
|
||||
// gi samme parser-HTML i begge motorer.
|
||||
describe('inline — lenker [tekst](url) (N3.5 MR-F8)', () => {
|
||||
test('renders [tekst](url) as a link with target/rel', () => {
|
||||
const out = inline('se [Reuters](https://www.reuters.com/x) for detaljer');
|
||||
assert.match(out, /<a href="https:\/\/www\.reuters\.com\/x"[^>]*>Reuters<\/a>/);
|
||||
assert.match(out, /target="_blank"/);
|
||||
assert.match(out, /rel="noopener"/);
|
||||
});
|
||||
|
||||
test('mailto is allowed', () => {
|
||||
assert.match(inline('[skriv](mailto:x@y.no)'), /<a href="mailto:x@y\.no"/);
|
||||
});
|
||||
|
||||
test('drops non-http(s)/mailto schemes — link text survives, href does not', () => {
|
||||
const out = inline('[klikk](javascript:alert(1))');
|
||||
assert.doesNotMatch(out, /<a /);
|
||||
assert.match(out, /klikk/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('markdownToHtml — flerlinjers blockquote (N3.5 MR-F8)', () => {
|
||||
test('a blockquote keeps its paragraph breaks', () => {
|
||||
const md = ['> Første avsnitt.', '>', '> Andre avsnitt.'].join('\n');
|
||||
const html = markdownToHtml(md);
|
||||
assert.match(html, /<blockquote>/);
|
||||
// two separate <p> inside the quote — the old parser collapsed these into one
|
||||
const paras = html.match(/<blockquote>(.*?)<\/blockquote>/s)[1].match(/<p>/g) || [];
|
||||
assert.equal(paras.length, 2);
|
||||
});
|
||||
|
||||
test('a single-paragraph blockquote still renders one <p>', () => {
|
||||
const html = markdownToHtml('> Bare ett avsnitt her.');
|
||||
const paras = html.match(/<blockquote>(.*?)<\/blockquote>/s)[1].match(/<p>/g) || [];
|
||||
assert.equal(paras.length, 1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('markdownToHtml — FIGUR→SVG (N3.5 MR-F8)', () => {
|
||||
test('a FIGUR blockquote with no matching SVG falls back to a plain blockquote', () => {
|
||||
const html = markdownToHtml('> **[FIGUR 9 — «finnes ikke»]**');
|
||||
assert.match(html, /<blockquote>/);
|
||||
assert.doesNotMatch(html, /<figure/);
|
||||
});
|
||||
|
||||
test('a FIGUR blockquote with matching figurer/figN*.svg inlines the SVG as <figure>', () => {
|
||||
const prev = process.cwd();
|
||||
process.chdir(path.join(__dirname, 'fixtures', 'figur-serie'));
|
||||
try {
|
||||
const html = markdownToHtml('> **[FIGUR 1 — «Demo-figur»]**');
|
||||
assert.match(html, /<figure class="fig">/);
|
||||
assert.match(html, /<svg/);
|
||||
assert.match(html, /<figcaption>Figur 1 — Demo-figur<\/figcaption>/);
|
||||
// xml-deklarasjonen i SVG-fila skal strippes før inlining
|
||||
assert.doesNotMatch(html, /<\?xml/);
|
||||
assert.doesNotMatch(html, /<blockquote>/);
|
||||
} finally {
|
||||
process.chdir(prev);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 100" role="img" aria-label="Demo-figur for FIGUR-markørtesten">
|
||||
<rect x="10" y="10" width="180" height="80" fill="none" stroke="#333" stroke-width="2"/>
|
||||
<text x="100" y="55" text-anchor="middle" font-size="14">fixture</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 333 B |
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue