Data

CodeBlock

Code in a <pre> with a file name, line numbers, highlighted lines and a Copy button.

#code#text#no-js

Demo

Preview
main.qwequillo
use "quillo-ui.qwe"route GET / =    <html>        <head><style>{ui_css}</style></head>        <body class="ui-root">            <Button href="/start">Get started</Button>        </body>    </html>
curl -O https://quillolang.org/components/quillo-ui.qwe
quillo.toml
[server]port = 8080workers = 4    # one per core is plenty for most sites; the event loop does the rest, so raise it only when profiling says so[database]url = "sqlite://app.db"
sql
SELECT id, title  FROM posts WHERE published ORDER BY created DESC;

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/code_block.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/code_block.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/code_block.qwe
let code_block_demo_source = `use "quillo-ui.qwe"route GET / =    <html>        <head><style>{ui_css}</style></head>        <body class="ui-root">            <Button href="/start">Get started</Button>        </body>    </html>`let code_block_demo_config = `[server]port = 8080workers = 4    # one per core is plenty for most sites; the event loop does the rest, so raise it only when profiling says so[database]url = "sqlite://app.db"`view CodeBlockDemo() =    <div.demo-stack>        <CodeBlock filename="main.qwe" language="quillo" line_numbers={true} highlight="1,7" code={code_block_demo_source}/>        <CodeBlock code="curl -O https://quillolang.org/components/quillo-ui.qwe"/>        <CodeBlock filename="quillo.toml" wrap={true} line_numbers={true} start={10} highlight={[12]}                   code={code_block_demo_config}/>        <CodeBlock language="sql" copy={false} code={"SELECT id, title\n  FROM posts\n WHERE published\n ORDER BY created DESC;"}/>    </div>

Source

ui/code_block.qwe
// CodeBlock: code to read and copy, in a <pre>, with line numbers, lines picked// out and a header naming the file.////   <CodeBlock filename="main.qwe" line_numbers={true} highlight="3,5-7" code={source}/>//   <CodeBlock language="sh" code="quillo run app.qwe --watch"/>//// code: the text, shown as it is. filename: a header above it; language: a small// label in the header ("sh", "quillo"). line_numbers: numbers down the left, which// are not copied with the text; start: the first one's number (1). highlight: the// lines to pick out, by their shown number: "3,5-7" or [3, 5, 6, 7] (each a <mark>,// so screen readers say so). copy (true): a Copy button that copies the code; it says// "Copied" for a moment, or "Could not copy" if the browser refuses (not https).// wrap: long lines wrap instead of scrolling sideways. max_height: a CSS height past// which it scrolls. label: what screen readers call it (the filename by default).import ui/theme.qwe use ui_clslet code_block_css = css`.ui-cb { min-width: 0; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-subtle);  color: var(--ui-fg); font: 14px/1.4 var(--ui-font); overflow: hidden; }.ui-cb-head { display: flex; align-items: center; gap: 10px; min-height: 38px; padding: 0 6px 0 14px;  border-bottom: 1px solid var(--ui-border); background: color-mix(in srgb, var(--ui-subtle) 60%, var(--ui-surface)); }.ui-cb-file { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;  font: 500 13px/1.3 var(--ui-mono); color: var(--ui-fg); }.ui-cb-lang { flex: none; padding: 2px 7px; border-radius: 4px; background: var(--ui-surface); color: var(--ui-muted);  font: 600 11px/1.4 var(--ui-font); letter-spacing: .04em; text-transform: uppercase; }.ui-cb-copy { display: inline-flex; align-items: center; gap: 6px; height: 28px; margin-left: auto; padding: 0 9px;  border: 1px solid transparent; border-radius: var(--ui-radius-sm); background: transparent; color: var(--ui-muted);  font: 500 12px/1 var(--ui-font); cursor: pointer; }.ui-cb-copy:hover { border-color: var(--ui-border); background: var(--ui-surface); color: var(--ui-fg); }.ui-cb-copy:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-cb-copy svg { width: 14px; height: 14px; flex: none; }.ui-cb-copy-ok { color: var(--ui-success); }.ui-cb-copy-bad { color: var(--ui-danger); }.ui-cb-body { display: flex; align-items: flex-start; }.ui-cb-float { order: 2; flex: none; margin: 7px 6px 0 0; }.ui-cb-pre { flex: 1; min-width: 0; margin: 0; padding: 12px 0; overflow: auto; background: transparent; color: var(--ui-fg);  font: 13px/1.6 var(--ui-mono); tab-size: 4; }.ui-cb-pre:focus-visible { outline: none; box-shadow: inset var(--ui-focus); }.ui-cb-pre code { display: block; min-width: max-content; font: inherit; }.ui-cb-wrap .ui-cb-pre code { min-width: 0; white-space: pre-wrap; overflow-wrap: anywhere; }.ui-cb-line { display: block; min-height: 1.6em; padding: 0 16px; background: none; color: inherit; }.ui-cb-hl { background: color-mix(in srgb, var(--ui-accent) 14%, transparent); box-shadow: inset 3px 0 0 var(--ui-accent); }.ui-cb-num { display: inline-block; min-width: 2.5ch; margin-right: 16px; color: var(--ui-muted); opacity: .75;  text-align: right; user-select: none; -webkit-user-select: none; }.ui-cb-hl .ui-cb-num { color: var(--ui-accent); opacity: 1; }.ui-cb-wrap .ui-cb-numbered .ui-cb-line { padding-left: calc(16px + 2.5ch + 16px); text-indent: calc(-2.5ch - 16px); }`// The line numbers in "3,5-7" (or a list of them), as a list of ints.fn code_block_lines(spec) -> list {    if spec == null { return [] }    if type_of(spec) == "list" { return spec }    flatten(split(str(spec), ",") | map((part) => {        let p = split(trim(part), "-")        let a = int(trim(p[0])) ?? null        let b = len(p) > 1 ? (int(trim(p[1])) ?? null) : a        a == null || b == null || b < a ? [] : range(a, b + 1)    }))}// The code's lines, without the empty one after a final line break.fn code_block_split(code: str) -> list {    let ls = split(replace(code, "\r\n", "\n"), "\n")    len(ls) > 1 && last(ls) == "" ? take(ls, len(ls) - 1) : ls}component CodeBlock(code: str, filename: str? = null, language: str? = null, line_numbers: bool = false,                    start: int = 1, highlight = null, copy: bool = true, wrap: bool = false,                    max_height: str? = null, label: str? = null, class: str? = null) {    style global code_block_css    state result = ""    // "", "ok" or "bad"    if result != "" { every(1500, () => { result = "" }) }    let lines = code_block_split(code)    let marked = code_block_lines(highlight)    let head = filename != null || language != null    let copy_button = copy && <button type="button"            class={ui_cls(["ui-cb-copy", !head && "ui-cb-float", result != "" && `ui-cb-copy-${result}`])}            aria-label={filename != null ? `Copy ${filename}` : "Copy code"}            @click={() => {                if js("isSecureContext") != true { result = "bad" } else {                    js("navigator.clipboard.writeText", code).then((_ = null) => { result = "ok" }, (_ = null) => { result = "bad" })                }            }}>        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"             stroke-linejoin="round" aria-hidden="true">            <path d={result == "ok" ? "M20 6 9 17l-5-5"                     : "M10 8h10a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H10a2 2 0 0 1-2-2V10a2 2 0 0 1 2-2ZM4 16a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2"}/>        </svg>        <span aria-hidden="true">{result == "ok" ? "Copied" : result == "bad" ? "Could not copy" : "Copy"}</span>    </button>    <figure class={ui_cls(["ui-cb", wrap && "ui-cb-wrap", class])} style="margin: 0">        {head && <figcaption.ui-cb-head>            {filename != null ? <span.ui-cb-file>{filename}</span> : <span.ui-cb-file></span>}            {language != null && <span.ui-cb-lang>{language}</span>}            {copy_button}        </figcaption>}        <div.ui-cb-body>            {!head && copy_button}            <pre.ui-cb-pre tabindex="0" aria-label={label ?? filename ?? "Code"}                 style={max_height != null ? `max-height: ${max_height}` : null}><code class={line_numbers ? "ui-cb-numbered" : null}>                {for [i, l] in enumerate(lines) => {                    let n = start + i                    let gutter = line_numbers && <span.ui-cb-num aria-hidden="true">{n}</span>                    index_of(marked, n) != null                        ? <mark.ui-cb-line.ui-cb-hl>{gutter}{l}</mark>                        : <span.ui-cb-line>{gutter}{l}</span>                }}            </code></pre>        </div>        <span.ui-sr role="status">{result == "ok" ? "Copied" : result == "bad" ? "Could not copy" : ""}</span>    </figure>}
Thread
55Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.