MarkdownView
A small Markdown subset (headings, emphasis, links, lists, code, quotes) rendered safely as markup, in Quillo, with no HTML let through.
Demo
Writing a component
Each component is one file you copy into your program. It brings its own styles, follows the system's light or dark theme, and needs a build step works as it is. See the gallery for every one.
What goes in
- A comment saying what it is
- Its CSS, as
let name_css = css - The
vieworcomponent
Views first: a component with nothing to remember is a view. It is built on what the browser already does.
- Read the README
- Write the test
- Press Tab to reach everything, and *escaped* stars stay
scripts/build.sh && scripts/test.sh buttonTags are shown as written: <script>alert(1)</script> and <b>not bold</b>. A line that ends in two spaces
breaks here. A bad link) is blocked, and a picture is text unless images are on.
Type some Markdown here, with a link and code.
Get it
It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.
$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/markdown_view.qweimport ui/theme.qwe use theme_cssuse ui/markdown_view.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
let markdown_view_demo_text = `# Writing a componentEach component is **one file** you copy into your program. It brings its *own styles*,follows the system's _light or dark_ theme, and ~~needs a build step~~ works as it is.See [the gallery](https://quillolang.org/components/ "Quillo UI") for every one.## What goes in1. A comment saying what it is2. Its CSS, as \`let name_css = css\`3. The \`view\` or \`component\`> Views first: a component with nothing to remember is a view.> It is built on what the browser already does.- [x] Read the README- [ ] Write the test- Press **Tab** to reach everything, and \\*escaped\\* stars stay\`\`\`shscripts/build.sh && scripts/test.sh button\`\`\`---Tags are shown as written: <script>alert(1)</script> and <b>not bold</b>.A line that ends in two spaces breaks here. A [bad link](javascript:alert(1)) is blocked, and  is text unless images are on.`component MarkdownViewDemoLive() { state text = "Type some **Markdown** here, with a [link](https://quillolang.org) and `code`." <div.demo-stack> <TextArea name="markdown-view-demo-input" label="Markdown" rows={3} value={text} on_input={(t) => { text = t }}/> <div style="padding: 12px 14px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm)"> <MarkdownView text={text} id="markdown-view-demo-live"/> </div> </div>}view MarkdownViewDemo() = <div.demo-stack> <MarkdownView text={markdown_view_demo_text} id="markdown-view-demo-doc"/> <MarkdownViewDemoLive/> </div>Source
// MarkdownView: a small Markdown subset rendered as markup, here in Quillo, with no// HTML let through: for text visitors write (comments, bios, notes) where the// builtin markdown() is more than wanted, and in the browser without a request.//// <MarkdownView text={comment.body}/>// <MarkdownView text={readme} images={true} size="lg"/>//// text: the Markdown. What it reads: headings (# to ######), paragraphs (a blank// line between them; a line ending in two spaces breaks the line), **bold** or// __bold__, *italic* or _italic_ (at word edges), ~~struck~~, `code`, [links](url),// lists with -, * or + and numbered 1. (one level), task items - [ ] and - [x],// > quotes (with blocks inside), ``` fenced code (with a language, as a class on the// <code>), and --- rules. A backslash keeps the character after it.// Everything else is text: tags are shown as written, never run. A javascript: link// is blocked by Quillo itself. images:  makes an <img> when true; off by// default, since an image can tell its server who read the text.// size: "sm", "md" (default) or "lg". id: the element's id.// markdown_view_blocks(text) gives the blocks as data, to render some other way.import ui/theme.qwe use ui_clslet markdown_view_css = css`.ui-md { font: 15px/1.6 var(--ui-font); color: var(--ui-fg); overflow-wrap: anywhere; }.ui-md-sm { font-size: 13px; }.ui-md-lg { font-size: 17px; line-height: 1.65; }.ui-md > :first-child { margin-top: 0; }.ui-md > :last-child { margin-bottom: 0; }.ui-md p, .ui-md ul, .ui-md ol, .ui-md blockquote, .ui-md pre { margin: 0 0 .9em; }.ui-md h1, .ui-md h2, .ui-md h3, .ui-md h4, .ui-md h5, .ui-md h6 { margin: 1.4em 0 .5em; font-weight: 650; line-height: 1.25; letter-spacing: -.01em; }.ui-md h1 { font-size: 1.7em; }.ui-md h2 { font-size: 1.4em; }.ui-md h3 { font-size: 1.2em; }.ui-md h4 { font-size: 1.05em; }.ui-md h5, .ui-md h6 { font-size: 1em; }.ui-md a { color: var(--ui-accent); text-decoration: underline; text-underline-offset: 2px; }.ui-md a:focus-visible { outline: none; border-radius: 3px; box-shadow: var(--ui-focus); }.ui-md strong { font-weight: 600; }.ui-md code { padding: .1em .35em; border-radius: 4px; background: var(--ui-subtle); font: .9em var(--ui-mono); }.ui-md pre { padding: 12px 14px; overflow: auto; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-subtle); font: .875em/1.55 var(--ui-mono); }.ui-md pre code { padding: 0; background: none; font: inherit; }.ui-md blockquote { margin-left: 0; padding: .2em 0 .2em 1em; border-left: 3px solid var(--ui-accent); color: var(--ui-muted); }.ui-md blockquote > :last-child { margin-bottom: 0; }.ui-md ul, .ui-md ol { padding-left: 1.5em; }.ui-md li { margin: .2em 0; }.ui-md li::marker { color: var(--ui-muted); }.ui-md-task { list-style: none; margin-left: -1.5em; }.ui-md-task input { margin: 0 .5em 0 0; vertical-align: -.1em; accent-color: var(--ui-accent); }.ui-md img { max-width: 100%; height: auto; border-radius: var(--ui-radius-sm); }.ui-md hr { margin: 1.5em 0; border: 0; border-top: 1px solid var(--ui-border); }.ui-md del { color: var(--ui-muted); }`// Whether a line starts a block of its own, which ends a paragraph before it.fn markdown_view_starts_block(t: str) -> bool = starts_with(t, "```") || starts_with(t, ">") || re.test(t, "^#{1,6}(\\s|$)") || re.test(t, "^[-*+]\\s+") || re.test(t, "^\\d+[.)]\\s+") || re.test(t, "^(-\\s*){3,}$|^(\\*\\s*){3,}$|^(_\\s*){3,}$")// The blocks of a Markdown text: { kind: "heading", level, text }, { kind: "para", text },// { kind: "code", lang, text }, { kind: "quote", blocks }, { kind: "list", ordered, start, items },// { kind: "rule" }.fn markdown_view_blocks(text: str) -> list { let lines = split(replace(text, "\r\n", "\n"), "\n") let n = len(lines) let blocks = [] let i = 0 while i < n { let t = trim(lines[i]) if t == "" { i += 1 } else if starts_with(t, "```") { let lang = trim(slice(t, 3, len(t))) let code = [] i += 1 while i < n && !starts_with(trim(lines[i]), "```") { code = code | push(lines[i]) i += 1 } i += 1 blocks = blocks | push({ kind: "code", lang, text: join(code, "\n") }) } else if re.test(t, "^#{1,6}(\\s|$)") { let m = re.find(t, "^(#{1,6})\\s*(.*?)\\s*#*$") blocks = blocks | push({ kind: "heading", level: len(m.groups[0]), text: m.groups[1] }) i += 1 } else if re.test(t, "^(-\\s*){3,}$|^(\\*\\s*){3,}$|^(_\\s*){3,}$") { blocks = blocks | push({ kind: "rule" }) i += 1 } else if starts_with(t, ">") { let inner = [] while i < n && starts_with(trim(lines[i]), ">") { inner = inner | push(re.replace(trim(lines[i]), "^>\\s?", "")) i += 1 } blocks = blocks | push({ kind: "quote", blocks: markdown_view_blocks(join(inner, "\n")) }) } else if re.test(t, "^[-*+]\\s+") { let items = [] while i < n && re.test(trim(lines[i]), "^[-*+]\\s+") { items = items | push(re.replace(trim(lines[i]), "^[-*+]\\s+", "")) i += 1 } blocks = blocks | push({ kind: "list", ordered: false, start: 1, items }) } else if re.test(t, "^\\d+[.)]\\s+") { let start = int(re.find(t, "^\\d+").text) ?? 1 let items = [] while i < n && re.test(trim(lines[i]), "^\\d+[.)]\\s+") { items = items | push(re.replace(trim(lines[i]), "^\\d+[.)]\\s+", "")) i += 1 } blocks = blocks | push({ kind: "list", ordered: true, start, items }) } else { let para = [] while i < n && trim(lines[i]) != "" && (len(para) == 0 || !markdown_view_starts_block(trim(lines[i]))) { para = para | push(re.replace(lines[i], "^\\s+", "")) i += 1 } blocks = blocks | push({ kind: "para", text: join(para, "\n") }) } } blocks}// A [label](url "title") or  starting at `i` (the "["): { label, url, title, end }, or null.fn markdown_view_link(text: str, i: int) -> map? { let n = len(text) let close = index_of(slice(text, i, n), "](") if close == null { return null } let label = slice(text, i + 1, i + close) let from = i + close + 2 let end = index_of(slice(text, from, n), ")") if end == null { return null } let target = trim(slice(text, from, from + end)) let titled = re.find(target, "^(\\S+)\\s+\"(.*)\"$") { label, url: titled != null ? titled.groups[0] : target, title: titled != null ? titled.groups[1] : null, end: from + end + 1 }}// A line of Markdown as a list of text and elements: code, bold, italic, struck, links, images, breaks.fn markdown_view_inline(text: str, images: bool) -> list { let n = len(text) let out = [] let run = "" let i = 0 while i < n { let c = text[i] let rest = slice(text, i + 1, n) let two = slice(text, i, i + 2) let before = i > 0 ? text[i - 1] : "" if c == "\\" && i + 1 < n { run = run + text[i + 1] i += 2 } else if c == "`" { let close = index_of(rest, "`") if close != null && close > 0 { out = out | push(run) | push(<code>{slice(rest, 0, close)}</code>) run = "" i = i + 2 + close } else { run = run + c i += 1 } } else if two == "**" || two == "__" || two == "~~" { let after = slice(text, i + 2, n) let close = index_of(after, two) if close != null && close > 0 && !starts_with(after, " ") && (two != "__" || !re.test(before, "\\w")) { let inner = markdown_view_inline(slice(after, 0, close), images) out = out | push(run) | push(two == "~~" ? <del>{inner}</del> : <strong>{inner}</strong>) run = "" i = i + 4 + close } else { run = run + two i += 2 } } else if c == "*" || c == "_" { let close = index_of(rest, c) let next = close != null && i + 2 + close < n ? text[i + 2 + close] : "" if close != null && close > 0 && !starts_with(rest, " ") && !ends_with(slice(rest, 0, close), " ") && (c != "_" || (!re.test(before, "\\w") && !re.test(next, "\\w"))) { out = out | push(run) | push(<em>{markdown_view_inline(slice(rest, 0, close), images)}</em>) run = "" i = i + 2 + close } else { run = run + c i += 1 } } else if c == "!" && starts_with(rest, "[") { let link = markdown_view_link(text, i + 1) if link != null && images { out = out | push(run) | push(<img src={link.url} alt={link.label} title={link.title}/>) run = "" i = link.end } else if link != null { run = run + link.label i = link.end } else { run = run + c i += 1 } } else if c == "[" { let link = markdown_view_link(text, i) if link != null { out = out | push(run) | push(<a href={link.url} title={link.title}>{markdown_view_inline(link.label, images)}</a>) run = "" i = link.end } else { run = run + c i += 1 } } else if c == "\n" { if ends_with(run, " ") { out = out | push(trim(run)) | push(<br/>) run = "" } else { run = run + " " } i += 1 } else { run = run + c i += 1 } } out | push(run) | filter((p) => p != "")}// The blocks as markup.fn markdown_view_render(blocks: list, images: bool) -> list = blocks | map((b) => match b.kind { "heading" => { let inner = markdown_view_inline(b.text, images) match b.level { 1 => <h1>{inner}</h1> 2 => <h2>{inner}</h2> 3 => <h3>{inner}</h3> 4 => <h4>{inner}</h4> 5 => <h5>{inner}</h5> _ => <h6>{inner}</h6> } } "para" => <p>{markdown_view_inline(b.text, images)}</p> "code" => <pre><code class={b.lang != "" ? `language-${b.lang}` : null}>{b.text}</code></pre> "quote" => <blockquote>{markdown_view_render(b.blocks, images)}</blockquote> "rule" => <hr/> "list" => { let items = for it in b.items => { let task = re.find(it, "^\\[( |x|X)\\]\\s+") task != null ? <li.ui-md-task> <input type="checkbox" checked={task.groups[0] != " "} disabled={true} aria-label={slice(it, task.end, len(it))}/> {markdown_view_inline(slice(it, task.end, len(it)), images)} </li> : <li>{markdown_view_inline(it, images)}</li> } b.ordered ? <ol start={b.start != 1 ? b.start : null}>{items}</ol> : <ul>{items}</ul> } _ => null })view MarkdownView(text: str, images: bool = false, size: str = "md", id: str? = null, class: str? = null) { style global markdown_view_css <div class={ui_cls(["ui-md", size != "md" && `ui-md-${size}`, class])} id={id}> {markdown_view_render(markdown_view_blocks(text), images)} </div>}Related
Thread 0 posts
On MarkdownView: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.