Typography

MarkdownView

A small Markdown subset (headings, emphasis, links, lists, code, quotes) rendered safely as markup, in Quillo, with no HTML let through.

#typography#text#document#code#no-js

Demo

Preview

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

  1. A comment saying what it is
  2. 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.

  • Read the README
  • Write the test
  • Press Tab to reach everything, and *escaped* stars stay
scripts/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) 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.qwe
main.qwe
import 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.

demos/markdown_view.qwe
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 ![a picture](https://example.com/x.png) 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

ui/markdown_view.qwe
// 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: ![alt](src) 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 ![alt](src) 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>}
Thread
49Useful? Like it, or add to its thread below.

Thread 0 posts

On MarkdownView: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.