gitoria
All repositories: gitoria
19.4 KB
// markdown.hl — copied from tickets.worldapi.org (markdown.hl, ticket tickets#6) for the repo homepage (gitoria#8), with tables, quotes and rules added. Originally: MARKDOWN IN TEXT FIELDS (mission 014; CONCEPT.md// "Data formats": Markdown inside text fields). The summary, comment texts and state notes are// Markdown; the ticket page renders them. Statics only, server realm.//// SAFE BY CONSTRUCTION: the text is parsed HERE into plain data — blocks of spans — and the// View (components/markdown.hl) builds elements from it. Every character of the source ends// up in a TEXT node (hl:web escapes it); there is no HTML string anywhere, so raw HTML in the// source (`<script>`, `<img onerror>`) is shown as the text it is. A link's href is taken only// if `safeHref` accepts it: http(s):, mailto:, a same-site path (`/…`, not `//…`) or `#…`;// anything else (`javascript:`, `data:`, `vbscript:`, relative `foo`, quotes, spaces, control// characters) stays plain text.//// WHAT IS UNDERSTOOD (the subset ticket #6 names — headings, lists, code, links, emphasis):// blocks: `#`…`######` headings, `-`/`*`/`+` and `1.`/`1)` list items (one level; deeper// indentation is flattened; a list may interrupt a paragraph — the import writes// them that way), ``` / ~~~ fenced code, paragraphs (blank-line separated; their// single line breaks are KEPT — the page showed them before, `white-space: pre-wrap`)// inline: `code` (any backtick run), [text](url), <https://…>, bare http(s):// URLs,// *em* _em_ **strong** __strong__ ***both***, backslash escapes of punctuation// Also understood here (gitoria#8): `> block quotes` (inline text only), | tables |, horizontal rules.// Not understood (shown as typed): images, nested lists, HTML.//// A block: { isP | isH1 | isH2 | isH3 | isUl | isOl | isCode | isQuote | isTable | isHr, head/rows (table: cells [{ spans }]), spans, items: [{ spans }],// start (ol), code (text of a code block) }// A span: { text, isText | isStrong | isEm | isStrongEm | isCode | isLink, href }static NL = ""static isSpaceC = (c) => { return c == 32 || c == 9 || c == 10 || c == 13 }static isAlnumC = (c) => { return (c >= 48 && c <= 57) || (c >= 65 && c <= 90) || (c >= 97 && c <= 122) || c > 127 }static isPunctC = (c) => { return (c >= 33 && c <= 47) || (c >= 58 && c <= 64) || (c >= 91 && c <= 96) || (c >= 123 && c <= 126) }// ---- links ------------------------------------------------------------------------------// the href of a link, or null when it is not one we let throughstatic safeHref = (u) => {if (u == null) { return null }let t = ('' + u).trim()if (t == '' || t.length > 2000) { return null }let i = 0while (i < t.length) {let c = t.charCodeAt(i)// no spaces / control characters, no quotes, no < > ` and no backslashif (c <= 32 || c == 127 || c == 34 || c == 39 || c == 60 || c == 62 || c == 96 || c == 92) { return null }i = i + 1}let low = t.toLowerCase()if (low.startsWith('https://') && t.length > 8) { return t }if (low.startsWith('http://') && t.length > 7) { return t }if (low.startsWith('mailto:') && t.length > 7) { return t }if (t.startsWith('/') && !t.startsWith('//')) { return t }if (t.startsWith('#')) { return t }return null}static isUrlStart = (s, i) => {let low = s.slice(i, i + 8).toLowerCase()return low.startsWith('https://') || low.startsWith('http://')}// where a bare URL starting at i ends: up to whitespace or < > " `, minus trailing punctuation// (and a closing parenthesis that has no opening one inside the URL)static isUrlStop = (c) => { return isSpaceC(c) || c == 60 || c == 62 || c == 34 || c == 96 }static urlEnd = (s, i) => {let end = iwhile (end < s.length && !isUrlStop(s.charCodeAt(end))) { end = end + 1 }let trimming = truewhile (trimming && end > i) {let c = s.charCodeAt(end - 1)if (c == 46 || c == 44 || c == 59 || c == 58 || c == 33 || c == 63 || c == 39 || c == 42 || c == 95) {end = end - 1} else if (c == 41) {let opens = 0let closes = 0let k = iwhile (k < end) {if (s.charCodeAt(k) == 40) { opens = opens + 1 }if (s.charCodeAt(k) == 41) { closes = closes + 1 }k = k + 1}if (closes > opens) { end = end - 1 } else { trimming = false }} else {trimming = false}}return end}// `[label](url)` at i → { label, href (null = not allowed), end } or null (not a link)static linkAt = (s, i) => {let j = i + 1let found = -1while (j < s.length && found < 0) {let c = s.charCodeAt(j)if (c == 91) { return null }if (c == 93) { found = j }j = j + 1}if (found < 0 || found == i + 1) { return null }if (found + 1 >= s.length || s.charCodeAt(found + 1) != 40) { return null }let k = s.indexOf(')', found + 2)if (k < 0) { return null }let target = s.slice(found + 2, k).trim()// an optional title after a space is dropped: [x](url "title")let sp = target.indexOf(' ')if (sp > 0) { target = target.slice(0, sp) }return { label = s.slice(i + 1, found) href = safeHref(target) end = k + 1 }}// ---- spans --------------------------------------------------------------------------------static textSpan = (t, strong, em) => {return { text = t isText = !strong && !em isStrong = strong && !em isEm = em && !strong isStrongEm = strong && em isCode = false isLink = false href = '' }}static codeSpan = (t) => {return { text = t isText = false isStrong = false isEm = false isStrongEm = false isCode = true isLink = false href = '' }}static linkSpan = (t, h) => {return { text = t isText = false isStrong = false isEm = false isStrongEm = false isCode = false isLink = true href = h }}// the index of a run of EXACTLY n characters c at or after `at`, or -1. `esc`: a character// after a backslash does not count (emphasis; inside `code` a backslash is just a backslash)static runAt = (s, at, c, n, esc) => {let i = atwhile (i < s.length) {if (esc && s.charCodeAt(i) == 92) {i = i + 2} else if (s.charCodeAt(i) == c) {let k = iwhile (k < s.length && s.charCodeAt(k) == c) { k = k + 1 }if (k - i == n) { return i }i = k} else {i = i + 1}}return -1}// is there a CLOSING delimiter run (exactly n of c, not after a space; `_` not before a// letter/digit) after `at`?static hasCloser = (s, at, c, n) => {let i = atwhile (i < s.length) {let r = runAt(s, i, c, n, true)if (r < 0) { return false }let prev = r > 0 ? s.charCodeAt(r - 1) : 32let next = r + n < s.length ? s.charCodeAt(r + n) : 32if (r > at && !isSpaceC(prev) && (c != 95 || !isAlnumC(next))) { return true }i = r + n}return false}static inlineSpans = (src) => {let out = []if (src == null || src == '') { return out }let s = '' + srclet n = s.lengthlet buf = ''let strong = falselet em = falselet strongC = 0let emC = 0let i = 0while (i < n) {let c = s.charCodeAt(i)if (c == 92 && i + 1 < n && isPunctC(s.charCodeAt(i + 1))) {// a backslash escape: the next punctuation character as itselfbuf = buf + s[i + 1]i = i + 2} else if (c == 96) {// `code`: a run of backticks up to the same runlet k = iwhile (k < n && s.charCodeAt(k) == 96) { k = k + 1 }let close = runAt(s, k, 96, k - i, false)if (close < 0) {buf = buf + s.slice(i, k)i = k} else {if (buf != '') { out.push(textSpan(buf, strong, em)) }buf = ''let code = s.slice(k, close).replaceAll(NL, ' ')if (code.length > 2 && code.startsWith(' ') && code.endsWith(' ') && code.trim() != '') { code = code.slice(1, code.length - 1) }out.push(codeSpan(code))i = close + (k - i)}} else if (c == 91) {// [label](url)let l = linkAt(s, i)if (l == null) {buf = buf + '['i = i + 1} else if (l.href == null) {buf = buf + s.slice(i, l.end)i = l.end} else {if (buf != '') { out.push(textSpan(buf, strong, em)) }buf = ''out.push(linkSpan(l.label, l.href))i = l.end}} else if (c == 60 && i + 1 < n && isUrlStart(s, i + 1)) {// <https://…>let e = s.indexOf('>', i + 1)let url = e > 0 ? s.slice(i + 1, e) : ''let h = safeHref(url)if (e < 0 || h == null) {buf = buf + '<'i = i + 1} else {if (buf != '') { out.push(textSpan(buf, strong, em)) }buf = ''out.push(linkSpan(url, h))i = e + 1}} else if ((c == 104 || c == 72) && (i == 0 || !isAlnumC(s.charCodeAt(i - 1))) && isUrlStart(s, i)) {// a bare http(s):// URLlet e = urlEnd(s, i)let url = s.slice(i, e)let h = safeHref(url)if (h == null) {buf = buf + s[i]i = i + 1} else {if (buf != '') { out.push(textSpan(buf, strong, em)) }buf = ''out.push(linkSpan(url, h))i = e}} else if (c == 42 || c == 95) {// * and _: emphasis (1), strong (2), both (3)let k = iwhile (k < n && s.charCodeAt(k) == c) { k = k + 1 }let run = k - ilet prev = i > 0 ? s.charCodeAt(i - 1) : 32let next = k < n ? s.charCodeAt(k) : 32let wantStrong = run == 2 || run == 3let wantEm = run == 1 || run == 3let canOpen = !isSpaceC(next) && (c == 42 || !isAlnumC(prev))let canClose = !isSpaceC(prev) && (c == 42 || !isAlnumC(next))let closes = canClose && run <= 3 && (!wantStrong || (strong && strongC == c)) && (!wantEm || (em && emC == c))let opens = !closes && canOpen && run <= 3 && (!wantStrong || !strong) && (!wantEm || !em) && hasCloser(s, k, c, run)if (closes || opens) {if (buf != '') { out.push(textSpan(buf, strong, em)) }buf = ''if (wantStrong) {strong = opensstrongC = c}if (wantEm) {em = opensemC = c}} else {buf = buf + s.slice(i, k)}i = k} else {buf = buf + s[i]i = i + 1}}if (buf != '') { out.push(textSpan(buf, strong, em)) }return out}// ---- blocks -------------------------------------------------------------------------------static noCr = (line) => { return line.length > 0 && line.charCodeAt(line.length - 1) == 13 ? line.slice(0, line.length - 1) : line }static indentOf = (line) => {let i = 0while (i < line.length && (line.charCodeAt(i) == 32 || line.charCodeAt(i) == 9)) { i = i + 1 }return i}// a heading line (≤ 3 spaces in, 1–6 '#', a space or the end) → { level, text } or nullstatic headingOf = (line) => {if (indentOf(line) > 3) { return null }let t = line.trim()let k = 0while (k < t.length && t.charCodeAt(k) == 35) { k = k + 1 }if (k == 0 || k > 6) { return null }if (k < t.length && t.charCodeAt(k) != 32 && t.charCodeAt(k) != 9) { return null }let body = t.slice(k).trim()// a closing run of '#' after a space goeslet e = body.lengthwhile (e > 0 && body.charCodeAt(e - 1) == 35) { e = e - 1 }if (e == 0) { body = '' } else if (e < body.length && (body.charCodeAt(e - 1) == 32 || body.charCodeAt(e - 1) == 9)) { body = body.slice(0, e).trim() }return { level = k text = body }}// a list item line → { ordered, start, text } or nullstatic itemOf = (line) => {let t = line.trim()if (t.length >= 2 && (t.charCodeAt(0) == 45 || t.charCodeAt(0) == 42 || t.charCodeAt(0) == 43) && (t.charCodeAt(1) == 32 || t.charCodeAt(1) == 9)) {return { ordered = false start = 1 text = t.slice(2).trim() }}if (t.length == 1 && (t == '-' || t == '*' || t == '+')) { return { ordered = false start = 1 text = '' } }let k = 0while (k < t.length && k < 9 && t.charCodeAt(k) >= 48 && t.charCodeAt(k) <= 57) { k = k + 1 }if (k == 0 || k >= t.length) { return null }let d = t.charCodeAt(k)if (d != 46 && d != 41) { return null }if (k + 1 < t.length && t.charCodeAt(k + 1) != 32 && t.charCodeAt(k + 1) != 9) { return null }return { ordered = true start = toNumber(t.slice(0, k)) text = t.slice(k + 1).trim() }}// a code fence line (``` or ~~~, ≥ 3) → its run (e.g. '````') or nullstatic fenceOf = (line) => {if (indentOf(line) > 3) { return null }let t = line.trim()if (t.length < 3) { return null }let c = t.charCodeAt(0)if (c != 96 && c != 126) { return null }let k = 0while (k < t.length && t.charCodeAt(k) == c) { k = k + 1 }if (k < 3) { return null }// an info string after ``` must not contain a backtickif (c == 96 && t.slice(k).includes('`')) { return null }return t.slice(0, k)}static paraBlock = (lines) => {let spans = inlineSpans(lines.join(NL))return { isP = true isH1 = false isH2 = false isH3 = false isUl = false isOl = false isCode = false isQuote = false isTable = false isHr = false spans = spans items = [] start = 1 code = '' head = [] rows = [] }}static headingBlock = (level, text) => {let spans = inlineSpans(text)return { isP = false isH1 = level == 1 isH2 = level == 2 isH3 = level >= 3 isUl = false isOl = false isCode = false isQuote = false isTable = false isHr = false spans = spans items = [] start = 1 code = '' head = [] rows = [] }}static codeBlock = (lines) => {let text = lines.join(NL)return { isP = false isH1 = false isH2 = false isH3 = false isUl = false isOl = false isCode = true isQuote = false isTable = false isHr = false spans = [] items = [] start = 1 code = text head = [] rows = [] }}static listBlock = (ordered, first, itemLines) => {let items = []for (lines of itemLines) {let spans = inlineSpans(lines.join(NL))items.push({ spans = spans })}return { isP = false isH1 = false isH2 = false isH3 = false isUl = !ordered isOl = ordered isCode = false isQuote = false isTable = false isHr = false spans = [] items = items start = first code = '' head = [] rows = [] }}static quoteBlock = (lines) => {let spans = inlineSpans(lines.join(NL))return { isP = false isH1 = false isH2 = false isH3 = false isUl = false isOl = false isCode = false isQuote = true isTable = false isHr = false spans = spans items = [] start = 1 code = '' head = [] rows = [] }}static hrBlock = () => {return { isP = false isH1 = false isH2 = false isH3 = false isUl = false isOl = false isCode = false isQuote = false isTable = false isHr = true spans = [] items = [] start = 1 code = '' head = [] rows = [] }}static tableBlock = (head, rows) => {return { isP = false isH1 = false isH2 = false isH3 = false isUl = false isOl = false isCode = false isQuote = false isTable = true isHr = false spans = [] items = [] start = 1 code = '' head = head rows = rows }}// a line of 3+ of one of - * _ (spaces between allowed) and nothing elsestatic isHrLine = (line) => {if (indentOf(line) > 3) { return false }let t = line.trim()if (t.length < 3) { return false }let c = t.charCodeAt(0)if (c != 45 && c != 42 && c != 95) { return false }let n = 0let i = 0while (i < t.length) {let d = t.charCodeAt(i)if (d == c) { n = n + 1 } else if (d != 32 && d != 9) { return false }i = i + 1}return n >= 3}// `> text` → the text after the marker, or nullstatic quoteOf = (line) => {if (indentOf(line) > 3) { return null }let t = line.trim()if (!t.startsWith('>')) { return null }return t.slice(1).trim()}// the cells of a table row line: `| a | b |` → [a, b] as spans listsstatic cellsOf = (line) => {let t = line.trim()if (t.startsWith('|')) { t = t.slice(1) }if (t.endsWith('|')) { t = t.slice(0, t.length - 1) }let out = []for (c of t.split('|')) { out.push({ spans = inlineSpans(c.trim()) }) }return out}// is this the `|---|:--:|` line under a table head?static isSepLine = (line) => {let t = line.trim()if (!t.includes('-') || !t.includes('|') && !t.includes(':')) { return false }let i = 0while (i < t.length) {let d = t.charCodeAt(i)if (d != 124 && d != 45 && d != 58 && d != 32 && d != 9) { return false }i = i + 1}return true}// the Markdown text → blocks (see the header)static markdownBlocks = (text) => {let out = []if (text == null || text == '') { return out }let lines = ('' + text).split(NL)let para = []let inList = falselet listOrdered = falselet listStart = 1let items = []let blankBefore = falselet i = 0while (i < lines.length) {let line = noCr(lines[i])let fence = fenceOf(line)let hr = fence == null && isHrLine(line)let q = fence == null && !hr ? quoteOf(line) : nulllet isTab = fence == null && !hr && q == null && line.includes('|') && i + 1 < lines.length && isSepLine(noCr(lines[i + 1])) && noCr(lines[i + 1]).includes('|')let h = fence == null && !hr && q == null && !isTab ? headingOf(line) : nulllet it = fence == null && !hr && q == null && !isTab && h == null ? itemOf(line) : nullif (hr) {if (para.length > 0) { out.push(paraBlock(para)) }para = []if (inList) { out.push(listBlock(listOrdered, listStart, items)) }inList = falseitems = []out.push(hrBlock())blankBefore = falsei = i + 1} else if (q != null) {if (para.length > 0) { out.push(paraBlock(para)) }para = []if (inList) { out.push(listBlock(listOrdered, listStart, items)) }inList = falseitems = []let ql = []let j = ilet more = truewhile (j < lines.length && more) {let x = quoteOf(noCr(lines[j]))if (x == null) { more = false } else {ql.push(x)j = j + 1}}out.push(quoteBlock(ql))i = jblankBefore = false} else if (isTab) {if (para.length > 0) { out.push(paraBlock(para)) }para = []if (inList) { out.push(listBlock(listOrdered, listStart, items)) }inList = falseitems = []let head = cellsOf(line)let rows = []let j = i + 2while (j < lines.length && noCr(lines[j]).trim() != '' && noCr(lines[j]).includes('|')) {rows.push(cellsOf(noCr(lines[j])))j = j + 1}out.push(tableBlock(head, rows))i = jblankBefore = false} else if (fence != null) {if (para.length > 0) { out.push(paraBlock(para)) }para = []if (inList) { out.push(listBlock(listOrdered, listStart, items)) }inList = falseitems = []let code = []let j = i + 1let closed = falselet fc = fence.charCodeAt(0)while (j < lines.length && !closed) {let l = noCr(lines[j])let f = fenceOf(l)if (f != null && f.charCodeAt(0) == fc && f.length >= fence.length && l.trim() == f) {closed = true} else {code.push(l)}j = j + 1}out.push(codeBlock(code))i = jblankBefore = false} else if (line.trim() == '') {if (para.length > 0) { out.push(paraBlock(para)) }para = []blankBefore = truei = i + 1} else if (h != null) {if (para.length > 0) { out.push(paraBlock(para)) }para = []if (inList) { out.push(listBlock(listOrdered, listStart, items)) }inList = falseitems = []out.push(headingBlock(h.level, h.text))blankBefore = falsei = i + 1} else if (it != null) {if (para.length > 0) { out.push(paraBlock(para)) }para = []if (inList && listOrdered != it.ordered) {out.push(listBlock(listOrdered, listStart, items))inList = falseitems = []}if (!inList) {inList = truelistOrdered = it.orderedlistStart = it.startitems = []}items.push([it.text])blankBefore = falsei = i + 1} else if (inList && (!blankBefore || indentOf(line) >= 2)) {// a wrapped item line (or an indented paragraph of the item): onto the last itemlet last = items[items.length - 1]last.push(line.trim())items[items.length - 1] = lastblankBefore = falsei = i + 1} else {if (inList) { out.push(listBlock(listOrdered, listStart, items)) }inList = falseitems = []para.push(line.trim())blankBefore = falsei = i + 1}}if (para.length > 0) { out.push(paraBlock(para)) }if (inList) { out.push(listBlock(listOrdered, listStart, items)) }return out}
Branches
- mainmain branch
Latest commits
- 5b46ac84antcolony#40: LOG.md — missions 069/072 are antcolony missions (report paths on Byrodin)mre
- 5602ff41gitoria: Hybriel master 190aa11d (fc838894 GC correctness, #127 mountKids by reference, #126, #48) — tracker README flat; gates 200/0, 46/0, 44/0mre
- e85eaf01gitoria: 069 round 2 — hybriel 1a096ad3 not adopted (Markdown SSR still grows); browser gate waits for the server-side logout before restartmre
- 09ce4f3fgitoria: mission 069 re-vendor hybriel 8efba065 stopped (big SSR pages grow + slow down); lambda audit clean; old vendor keptmre
- 3dc43108antcolony#40: mission references point to the moved missionsmre
- 8d9450fdantcolony#40: history (LOG.md), worker briefs (missions/) and reports moved here from antcolony, numbered per project; old numbers in antcolony docs/mission-map.mdmre
- 205d5fe4gitoria: Hybriel master ff51cf46; ssh keys/tokens no double rows (session sync); gates follow #20mre
- 9b27cb26gitoria#21: installable app (manifest, service worker, offline start page), own iconmre
- 68dcb603deploy.sh: back up live storage/.sessions/.env before every deploy (newest 5 kept)mre
- e2deed6dgitoria#20: "Add code" only on the Code page of an empty repository, no collapsiblemre
- 8bb97ffddeploy.sh: never send .git or .gitignore to Byrodinmre
- fd981932State of 2026-09-27; bin/ no longer tracked (Hybriel commit is in README)mre
- 4a2d7125initial commitmre