/', () => {
+ const md = [
+ '| Plattform | Pris |',
+ '| --- | --- |',
+ '| Azure | Høy |',
+ '| Foundry | Lav |',
+ ].join('\n');
+ const html = markdownToHtml(md);
+ assert.match(html, //);
+ assert.match(html, //);
+ assert.match(html, /| /);
+ // header cells render as | , body values as |
+ assert.match(html, / | Plattform<\/th>/);
+ assert.match(html, / | Azure<\/td>/);
+ // the separator row must NOT become a data row
+ assert.doesNotMatch(html, / | ---<\/td>/);
+ });
+
+ test('empty input produces no table', () => {
+ assert.doesNotMatch(markdownToHtml(''), //);
+ });
+
+ test('tolerates a malformed row (uneven cell count) without throwing', () => {
+ const md = [
+ '| a | b |',
+ '| --- | --- |',
+ '| only-one-cell |',
+ '| x | y |',
+ ].join('\n');
+ let html;
+ assert.doesNotThrow(() => { html = markdownToHtml(md); });
+ assert.match(html, //);
+ assert.match(html, /x<\/td>/);
+ });
+});
+
+describe('markdownToHtml — heading levels # … #### (beslutning H)', () => {
+ test('# renders ', () => {
+ assert.match(markdownToHtml('# Tittel'), /Tittel<\/h1>/);
+ });
+ test('## renders ', () => {
+ assert.match(markdownToHtml('## Seksjon'), /Seksjon<\/h2>/);
+ });
+ test('### renders ', () => {
+ assert.match(markdownToHtml('### Under'), /Under<\/h3>/);
+ });
+ test('#### renders ', () => {
+ assert.match(markdownToHtml('#### Detalj'), /Detalj<\/h4>/);
+ });
+});
+
+describe('inline — backtick code span (beslutning H)', () => {
+ test('`code` renders ', () => {
+ assert.match(inline('bruk `npm install` her'), /npm install<\/code>/);
+ });
+ test('still handles **bold** and *italic*', () => {
+ assert.match(inline('**fet** og *kursiv*'), /fet<\/strong>/);
+ assert.match(inline('**fet** og *kursiv*'), /kursiv<\/em>/);
+ });
+});
diff --git a/plugins/linkedin-thought-leadership/render/build-html.mjs b/plugins/linkedin-thought-leadership/render/build-html.mjs
index ce72929..9523994 100644
--- a/plugins/linkedin-thought-leadership/render/build-html.mjs
+++ b/plugins/linkedin-thought-leadership/render/build-html.mjs
@@ -42,24 +42,87 @@ function esc(s) {
}
// ---------------------------------------------------------------------------
-// Inline markdown: **fet**, *kursiv*. «» og — beholdes uendret.
+// Inline markdown: `kode`, **fet**, *kursiv*. «» og — beholdes uendret.
// Tar uescapet tekst, returnerer escaped HTML med inline-tagger.
// ---------------------------------------------------------------------------
-function inline(text) {
+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) => `${c}`);
// **fet** før *kursiv* for å unngå konflikt
out = out.replace(/\*\*([^*]+)\*\*/g, (_, c) => `${c}`);
out = out.replace(/\*([^*]+)\*/g, (_, c) => `${c}`);
return out;
}
+// ---------------------------------------------------------------------------
+// Overskrift: # -> ... #### -> (klemmes til h4; dypere nivåer
+// kollapser til ). Tar antall #-tegn + raw heading-tekst.
+// ---------------------------------------------------------------------------
+export function renderHeading(hashes, text) {
+ const level = Math.min(hashes, 4); // # .. #### -> ..
+ return `${inline(text.trim())}`;
+}
+
+// ---------------------------------------------------------------------------
+// Tabell: en sammenhengende blokk av `| a | b |`-rader -> .
+// Rad 1 = header (| ). En påfølgende separator-rad (| --- | --- |) hoppes
+// over. Øvrige rader = | . Ujevn celletall tolereres (ingen kast).
+// ---------------------------------------------------------------------------
+function splitRow(line) {
+ // Fjern ledende/avsluttende pipe, del på resten.
+ return line
+ .trim()
+ .replace(/^\|/, "")
+ .replace(/\|$/, "")
+ .split("|")
+ .map((c) => c.trim());
+}
+function isSeparatorRow(line) {
+ // | --- | :---: | ---: | osv.
+ return /^\|?\s*:?-{1,}:?\s*(\|\s*:?-{1,}:?\s*)*\|?$/.test(line.trim());
+}
+export function renderTable(rows) {
+ if (!rows.length) return "";
+ let bodyRows = rows;
+ const header = splitRow(rows[0]);
+ let startIdx = 1;
+ if (rows.length > 1 && isSeparatorRow(rows[1])) startIdx = 2;
+ bodyRows = rows.slice(startIdx);
+
+ const thead =
+ "" +
+ header.map((c) => `| ${inline(c)} | `).join("") +
+ " ";
+ const tbody =
+ " | " +
+ bodyRows
+ .map(
+ (r) =>
+ "" +
+ splitRow(r)
+ .map((c) => `| ${inline(c)} | `)
+ .join("") +
+ " "
+ )
+ .join("") +
+ "";
+ return ``;
+}
+
+// A markdown table line: starts with `|` and has at least one more `|`.
+function isTableLine(t) {
+ return /^\|.*\|/.test(t);
+}
+
// ---------------------------------------------------------------------------
// Kompakt markdown -> HTML for body.
-// Håndterer: ## / ### overskrifter, - punktlister, 1. nummererte lister,
-// > blockquote, --- horisontal linje, og avsnitt (blanklinje-separert).
+// Håndterer: # .. #### overskrifter, | tabeller |, - punktlister,
+// 1. nummererte lister, > blockquote, --- horisontal linje, `kode`, og
+// avsnitt (blanklinje-separert).
// Første avsnitt får drop-cap-klasse. Avsnitt etter det første: .indent.
// ---------------------------------------------------------------------------
-function markdownToHtml(body) {
+export function markdownToHtml(body) {
const lines = body.replace(/\r\n/g, "\n").split("\n");
const blocks = [];
let i = 0;
@@ -85,15 +148,25 @@ function markdownToHtml(body) {
continue;
}
- // Overskrifter
- let hm = trimmed.match(/^(#{2,3})\s+(.*)$/);
+ // Overskrifter (# .. ###### -> .. , dypere klemmes til h4)
+ let hm = trimmed.match(/^(#{1,6})\s+(.*)$/);
if (hm) {
- const level = hm[1].length; // 2 eller 3
- blocks.push(`${inline(hm[2].trim())}`);
+ blocks.push(renderHeading(hm[1].length, hm[2]));
i++;
continue;
}
+ // Tabell (sammenhengende | a | b | -rader)
+ if (isTableLine(trimmed)) {
+ const rows = [];
+ while (i < lines.length && isTableLine(lines[i].trim())) {
+ rows.push(lines[i].trim());
+ i++;
+ }
+ blocks.push(renderTable(rows));
+ continue;
+ }
+
// Blockquote (sammenhengende > -linjer)
if (/^>\s?/.test(trimmed)) {
const qbuf = [];
@@ -144,7 +217,8 @@ function markdownToHtml(body) {
if (
t === "" ||
/^---+$/.test(t) ||
- /^(#{2,3})\s+/.test(t) ||
+ /^(#{1,6})\s+/.test(t) ||
+ isTableLine(t) ||
/^>\s?/.test(t) ||
/^[-*]\s+/.test(t) ||
/^\d+\.\s+/.test(t)
@@ -259,8 +333,31 @@ h1.title {
color: var(--accent);
font-weight: 700;
}
+.body h1 { font-size: 1.8rem; font-weight: 700; margin: 2.2rem 0 0.7rem; line-height: 1.15; }
.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 code {
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
+ font-size: 0.86em;
+ background: #efece4;
+ padding: 0.08em 0.35em;
+ border-radius: 3px;
+}
+.body table {
+ border-collapse: collapse;
+ width: 100%;
+ margin: 1.4rem 0;
+ font-family: var(--sans);
+ font-size: 0.86rem;
+}
+.body th, .body td {
+ border: 1px solid var(--rule);
+ padding: 0.5em 0.7em;
+ text-align: left;
+ vertical-align: top;
+}
+.body th { background: #efece4; font-weight: 600; }
.body ul, .body ol { margin: 0.8rem 0; padding-left: 1.5em; }
.body li { margin: 0.3rem 0; }
.body blockquote {
@@ -933,7 +1030,7 @@ ${CLIENT_JS}
// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------
-function main() {
+export function main() {
const args = process.argv.slice(2);
if (!args.length) {
console.error("Bruk: node build-html.mjs [flere.md ...]");
@@ -960,4 +1057,8 @@ function main() {
}
}
-main();
+// CLI-guard: kjør kun når scriptet startes direkte, ikke ved import
+// (mønster fra hooks/scripts/state-updater.mjs).
+if (import.meta.url === `file://${process.argv[1]}`) {
+ main();
+}
| | |