Spaces:
Running
Running
Deploy 4ed0390343adb220188e58f95ab1a8e7e1dd995e (manual: Actions blocked on billing) (part 2)
1b9ed71 verified | /* Just enough Markdown for the template notebooks. | |
| * | |
| * Surveyed across all 249 markdown cells in the corpus, the prose uses eight | |
| * constructs and no more: headings, inline code, bold, italic, bullet lists, | |
| * numbered lists, fenced code, and paragraphs. No tables, no links, no raw | |
| * HTML, no math, no blockquotes. | |
| * | |
| * So this renders those eight and nothing else, rather than pulling in a | |
| * library. The pages ship `script-src 'self'` with no bundler, so a dependency | |
| * here means another vendored file to keep current β a poor trade for 60 lines. | |
| * If a future template needs tables, add them; do not reach for marked.js on | |
| * the strength of one cell. | |
| * | |
| * Everything is escaped before any markup is inserted. The corpus is committed | |
| * and comes from a swan checkout, so this is not the security boundary that | |
| * matters β but a renderer that emits unescaped input is the kind of thing that | |
| * gets copied somewhere it does matter, and `<` appearing literally in prose | |
| * about generics should not silently eat the rest of a paragraph either. | |
| */ | |
| /* The repo's own escaper, not a second one. check_escaping.mjs recognises | |
| * esc()/escAttr()/escHtml() by name, so a private equivalent here is both a | |
| * duplicate and invisible to the check that exists to catch exactly this. */ | |
| export { esc as escapeHtml } from './util.js'; | |
| import { esc as escapeHtml } from './util.js'; | |
| /* The notebook cells contain no HTML β surveyed, all 249 of them. The READMEs | |
| * do: several wrap their troubleshooting in <details>/<summary>, which escaping | |
| * faithfully rendered as literal "<details><summary>" down the page. | |
| * | |
| * Dropped rather than honoured, and only these tags by name. Their text is | |
| * content and is kept; the collapsing is presentation this page does not need. | |
| * A blanket tag-stripper would eat the "<" in prose about generics, and letting | |
| * them through as real markup would mean rendering unescaped input β which this | |
| * file deliberately never does. */ | |
| const STRUCTURAL = /<\/?(?:details|summary|br|div|p)\b[^>]*>/gi; | |
| const stripStructural = (s) => String(s).replace(STRUCTURAL, ''); | |
| /** Inline spans, applied to already-escaped text. */ | |
| function inline(text) { | |
| return text | |
| // Code first, so ** or * inside a code span is not read as emphasis. | |
| .replace(/`([^`]+)`/g, (_, code) => `<code>${code}</code>`) | |
| .replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>') | |
| .replace(/(^|[^*])\*([^*\n]+)\*/g, '$1<em>$2</em>'); | |
| } | |
| /** | |
| * Render a markdown cell to HTML. | |
| * | |
| * Line-based rather than a parser: the grammar above has no nesting to track, | |
| * and a line-based reader is one that can be read in full before trusting it. | |
| */ | |
| export function renderMarkdown(src) { | |
| const lines = String(src).replace(/\r\n/g, '\n').split('\n'); | |
| const out = []; | |
| let para = []; // paragraph lines awaiting a blank line | |
| let list = null; // 'ul' | 'ol' while one is open | |
| let fence = null; // code fence body while one is open | |
| const flushPara = () => { | |
| if (!para.length) return; | |
| out.push(`<p>${inline(escapeHtml(para.join(' ')))}</p>`); | |
| para = []; | |
| }; | |
| const closeList = () => { | |
| if (list) { out.push(`</${list}>`); list = null; } | |
| }; | |
| for (const raw of lines) { | |
| // Prose only. Inside a fence the text is code, and a template showing how | |
| // to emit a <div> should keep its <div>. | |
| const line = fence === null ? stripStructural(raw) : raw; | |
| if (fence !== null) { | |
| if (/^\s*```/.test(line)) { | |
| out.push(`<pre class="md-code">${escapeHtml(fence.join('\n'))}</pre>`); | |
| fence = null; | |
| } else fence.push(line); | |
| continue; | |
| } | |
| if (/^\s*```/.test(line)) { flushPara(); closeList(); fence = []; continue; } | |
| if (!line.trim()) { flushPara(); closeList(); continue; } | |
| const heading = line.match(/^(#{1,6})\s+(.*)$/); | |
| if (heading) { | |
| flushPara(); closeList(); | |
| // Notebook headings start at h1 and would outrank the page's own title, | |
| // so everything shifts down one and stops at h6. | |
| const level = Math.min(heading[1].length + 1, 6); | |
| out.push(`<h${level}>${inline(escapeHtml(heading[2].trim()))}</h${level}>`); | |
| continue; | |
| } | |
| const bullet = line.match(/^\s*[-*+]\s+(.*)$/); | |
| const numbered = line.match(/^\s*\d+[.)]\s+(.*)$/); | |
| if (bullet || numbered) { | |
| flushPara(); | |
| const want = bullet ? 'ul' : 'ol'; | |
| if (list !== want) { closeList(); out.push(`<${want}>`); list = want; } | |
| out.push(`<li>${inline(escapeHtml((bullet || numbered)[1]))}</li>`); | |
| continue; | |
| } | |
| closeList(); | |
| para.push(line.trim()); | |
| } | |
| // An unterminated fence still renders its body: dropping it would lose prose | |
| // over a missing three characters. | |
| if (fence !== null) out.push(`<pre class="md-code">${escapeHtml(fence.join('\n'))}</pre>`); | |
| flushPara(); | |
| closeList(); | |
| return out.join('\n'); | |
| } | |
| /** The first paragraph, as plain text β for cards and summaries. */ | |
| export function firstParagraph(src) { | |
| for (const block of String(src).replace(/\r\n/g, '\n').split(/\n\s*\n/)) { | |
| const text = block.trim(); | |
| if (!text || text.startsWith('#') || text.startsWith('```')) continue; | |
| return inline(escapeHtml(text.replace(/\n/g, ' '))) | |
| .replace(/<[^>]+>/g, '') // strip the markup back off | |
| .replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>') | |
| .replace(/"/g, '"'); | |
| } | |
| return ''; | |
| } | |