gitoriaLog in with ident

gitoria

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Commit5602ff415602ff41gitoria: Hybriel master 190aa11d (fc838894 GC correctness, #127 mountKids by reference, #126, #48) — tracker README flat; gates 200/0, 46/0, 44/0mre5602ff41/markdown.hl

19.4 KB

  1. // 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
  2. // "Data formats": Markdown inside text fields). The summary, comment texts and state notes are
  3. // Markdown; the ticket page renders them. Statics only, server realm.
  4. //
  5. // SAFE BY CONSTRUCTION: the text is parsed HERE into plain data — blocks of spans — and the
  6. // View (components/markdown.hl) builds elements from it. Every character of the source ends
  7. // up in a TEXT node (hl:web escapes it); there is no HTML string anywhere, so raw HTML in the
  8. // source (`<script>`, `<img onerror>`) is shown as the text it is. A link's href is taken only
  9. // if `safeHref` accepts it: http(s):, mailto:, a same-site path (`/…`, not `//…`) or `#…`;
  10. // anything else (`javascript:`, `data:`, `vbscript:`, relative `foo`, quotes, spaces, control
  11. // characters) stays plain text.
  12. //
  13. // WHAT IS UNDERSTOOD (the subset ticket #6 names — headings, lists, code, links, emphasis):
  14. // blocks: `#`…`######` headings, `-`/`*`/`+` and `1.`/`1)` list items (one level; deeper
  15. // indentation is flattened; a list may interrupt a paragraph — the import writes
  16. // them that way), ``` / ~~~ fenced code, paragraphs (blank-line separated; their
  17. // single line breaks are KEPT — the page showed them before, `white-space: pre-wrap`)
  18. // inline: `code` (any backtick run), [text](url), <https://…>, bare http(s):// URLs,
  19. // *em* _em_ **strong** __strong__ ***both***, backslash escapes of punctuation
  20. // Also understood here (gitoria#8): `> block quotes` (inline text only), | tables |, horizontal rules.
  21. // Not understood (shown as typed): images, nested lists, HTML.
  22. //
  23. // A block: { isP | isH1 | isH2 | isH3 | isUl | isOl | isCode | isQuote | isTable | isHr, head/rows (table: cells [{ spans }]), spans, items: [{ spans }],
  24. // start (ol), code (text of a code block) }
  25. // A span: { text, isText | isStrong | isEm | isStrongEm | isCode | isLink, href }
  26. static NL = "
  27. "
  28. static isSpaceC = (c) => { return c == 32 || c == 9 || c == 10 || c == 13 }
  29. static isAlnumC = (c) => { return (c >= 48 && c <= 57) || (c >= 65 && c <= 90) || (c >= 97 && c <= 122) || c > 127 }
  30. static isPunctC = (c) => { return (c >= 33 && c <= 47) || (c >= 58 && c <= 64) || (c >= 91 && c <= 96) || (c >= 123 && c <= 126) }
  31. // ---- links ------------------------------------------------------------------------------
  32. // the href of a link, or null when it is not one we let through
  33. static safeHref = (u) => {
  34. if (u == null) { return null }
  35. let t = ('' + u).trim()
  36. if (t == '' || t.length > 2000) { return null }
  37. let i = 0
  38. while (i < t.length) {
  39. let c = t.charCodeAt(i)
  40. // no spaces / control characters, no quotes, no < > ` and no backslash
  41. if (c <= 32 || c == 127 || c == 34 || c == 39 || c == 60 || c == 62 || c == 96 || c == 92) { return null }
  42. i = i + 1
  43. }
  44. let low = t.toLowerCase()
  45. if (low.startsWith('https://') && t.length > 8) { return t }
  46. if (low.startsWith('http://') && t.length > 7) { return t }
  47. if (low.startsWith('mailto:') && t.length > 7) { return t }
  48. if (t.startsWith('/') && !t.startsWith('//')) { return t }
  49. if (t.startsWith('#')) { return t }
  50. return null
  51. }
  52. static isUrlStart = (s, i) => {
  53. let low = s.slice(i, i + 8).toLowerCase()
  54. return low.startsWith('https://') || low.startsWith('http://')
  55. }
  56. // where a bare URL starting at i ends: up to whitespace or < > " `, minus trailing punctuation
  57. // (and a closing parenthesis that has no opening one inside the URL)
  58. static isUrlStop = (c) => { return isSpaceC(c) || c == 60 || c == 62 || c == 34 || c == 96 }
  59. static urlEnd = (s, i) => {
  60. let end = i
  61. while (end < s.length && !isUrlStop(s.charCodeAt(end))) { end = end + 1 }
  62. let trimming = true
  63. while (trimming && end > i) {
  64. let c = s.charCodeAt(end - 1)
  65. if (c == 46 || c == 44 || c == 59 || c == 58 || c == 33 || c == 63 || c == 39 || c == 42 || c == 95) {
  66. end = end - 1
  67. } else if (c == 41) {
  68. let opens = 0
  69. let closes = 0
  70. let k = i
  71. while (k < end) {
  72. if (s.charCodeAt(k) == 40) { opens = opens + 1 }
  73. if (s.charCodeAt(k) == 41) { closes = closes + 1 }
  74. k = k + 1
  75. }
  76. if (closes > opens) { end = end - 1 } else { trimming = false }
  77. } else {
  78. trimming = false
  79. }
  80. }
  81. return end
  82. }
  83. // `[label](url)` at i → { label, href (null = not allowed), end } or null (not a link)
  84. static linkAt = (s, i) => {
  85. let j = i + 1
  86. let found = -1
  87. while (j < s.length && found < 0) {
  88. let c = s.charCodeAt(j)
  89. if (c == 91) { return null }
  90. if (c == 93) { found = j }
  91. j = j + 1
  92. }
  93. if (found < 0 || found == i + 1) { return null }
  94. if (found + 1 >= s.length || s.charCodeAt(found + 1) != 40) { return null }
  95. let k = s.indexOf(')', found + 2)
  96. if (k < 0) { return null }
  97. let target = s.slice(found + 2, k).trim()
  98. // an optional title after a space is dropped: [x](url "title")
  99. let sp = target.indexOf(' ')
  100. if (sp > 0) { target = target.slice(0, sp) }
  101. return { label = s.slice(i + 1, found) href = safeHref(target) end = k + 1 }
  102. }
  103. // ---- spans --------------------------------------------------------------------------------
  104. static textSpan = (t, strong, em) => {
  105. return { text = t isText = !strong && !em isStrong = strong && !em isEm = em && !strong isStrongEm = strong && em isCode = false isLink = false href = '' }
  106. }
  107. static codeSpan = (t) => {
  108. return { text = t isText = false isStrong = false isEm = false isStrongEm = false isCode = true isLink = false href = '' }
  109. }
  110. static linkSpan = (t, h) => {
  111. return { text = t isText = false isStrong = false isEm = false isStrongEm = false isCode = false isLink = true href = h }
  112. }
  113. // the index of a run of EXACTLY n characters c at or after `at`, or -1. `esc`: a character
  114. // after a backslash does not count (emphasis; inside `code` a backslash is just a backslash)
  115. static runAt = (s, at, c, n, esc) => {
  116. let i = at
  117. while (i < s.length) {
  118. if (esc && s.charCodeAt(i) == 92) {
  119. i = i + 2
  120. } else if (s.charCodeAt(i) == c) {
  121. let k = i
  122. while (k < s.length && s.charCodeAt(k) == c) { k = k + 1 }
  123. if (k - i == n) { return i }
  124. i = k
  125. } else {
  126. i = i + 1
  127. }
  128. }
  129. return -1
  130. }
  131. // is there a CLOSING delimiter run (exactly n of c, not after a space; `_` not before a
  132. // letter/digit) after `at`?
  133. static hasCloser = (s, at, c, n) => {
  134. let i = at
  135. while (i < s.length) {
  136. let r = runAt(s, i, c, n, true)
  137. if (r < 0) { return false }
  138. let prev = r > 0 ? s.charCodeAt(r - 1) : 32
  139. let next = r + n < s.length ? s.charCodeAt(r + n) : 32
  140. if (r > at && !isSpaceC(prev) && (c != 95 || !isAlnumC(next))) { return true }
  141. i = r + n
  142. }
  143. return false
  144. }
  145. static inlineSpans = (src) => {
  146. let out = []
  147. if (src == null || src == '') { return out }
  148. let s = '' + src
  149. let n = s.length
  150. let buf = ''
  151. let strong = false
  152. let em = false
  153. let strongC = 0
  154. let emC = 0
  155. let i = 0
  156. while (i < n) {
  157. let c = s.charCodeAt(i)
  158. if (c == 92 && i + 1 < n && isPunctC(s.charCodeAt(i + 1))) {
  159. // a backslash escape: the next punctuation character as itself
  160. buf = buf + s[i + 1]
  161. i = i + 2
  162. } else if (c == 96) {
  163. // `code`: a run of backticks up to the same run
  164. let k = i
  165. while (k < n && s.charCodeAt(k) == 96) { k = k + 1 }
  166. let close = runAt(s, k, 96, k - i, false)
  167. if (close < 0) {
  168. buf = buf + s.slice(i, k)
  169. i = k
  170. } else {
  171. if (buf != '') { out.push(textSpan(buf, strong, em)) }
  172. buf = ''
  173. let code = s.slice(k, close).replaceAll(NL, ' ')
  174. if (code.length > 2 && code.startsWith(' ') && code.endsWith(' ') && code.trim() != '') { code = code.slice(1, code.length - 1) }
  175. out.push(codeSpan(code))
  176. i = close + (k - i)
  177. }
  178. } else if (c == 91) {
  179. // [label](url)
  180. let l = linkAt(s, i)
  181. if (l == null) {
  182. buf = buf + '['
  183. i = i + 1
  184. } else if (l.href == null) {
  185. buf = buf + s.slice(i, l.end)
  186. i = l.end
  187. } else {
  188. if (buf != '') { out.push(textSpan(buf, strong, em)) }
  189. buf = ''
  190. out.push(linkSpan(l.label, l.href))
  191. i = l.end
  192. }
  193. } else if (c == 60 && i + 1 < n && isUrlStart(s, i + 1)) {
  194. // <https://…>
  195. let e = s.indexOf('>', i + 1)
  196. let url = e > 0 ? s.slice(i + 1, e) : ''
  197. let h = safeHref(url)
  198. if (e < 0 || h == null) {
  199. buf = buf + '<'
  200. i = i + 1
  201. } else {
  202. if (buf != '') { out.push(textSpan(buf, strong, em)) }
  203. buf = ''
  204. out.push(linkSpan(url, h))
  205. i = e + 1
  206. }
  207. } else if ((c == 104 || c == 72) && (i == 0 || !isAlnumC(s.charCodeAt(i - 1))) && isUrlStart(s, i)) {
  208. // a bare http(s):// URL
  209. let e = urlEnd(s, i)
  210. let url = s.slice(i, e)
  211. let h = safeHref(url)
  212. if (h == null) {
  213. buf = buf + s[i]
  214. i = i + 1
  215. } else {
  216. if (buf != '') { out.push(textSpan(buf, strong, em)) }
  217. buf = ''
  218. out.push(linkSpan(url, h))
  219. i = e
  220. }
  221. } else if (c == 42 || c == 95) {
  222. // * and _: emphasis (1), strong (2), both (3)
  223. let k = i
  224. while (k < n && s.charCodeAt(k) == c) { k = k + 1 }
  225. let run = k - i
  226. let prev = i > 0 ? s.charCodeAt(i - 1) : 32
  227. let next = k < n ? s.charCodeAt(k) : 32
  228. let wantStrong = run == 2 || run == 3
  229. let wantEm = run == 1 || run == 3
  230. let canOpen = !isSpaceC(next) && (c == 42 || !isAlnumC(prev))
  231. let canClose = !isSpaceC(prev) && (c == 42 || !isAlnumC(next))
  232. let closes = canClose && run <= 3 && (!wantStrong || (strong && strongC == c)) && (!wantEm || (em && emC == c))
  233. let opens = !closes && canOpen && run <= 3 && (!wantStrong || !strong) && (!wantEm || !em) && hasCloser(s, k, c, run)
  234. if (closes || opens) {
  235. if (buf != '') { out.push(textSpan(buf, strong, em)) }
  236. buf = ''
  237. if (wantStrong) {
  238. strong = opens
  239. strongC = c
  240. }
  241. if (wantEm) {
  242. em = opens
  243. emC = c
  244. }
  245. } else {
  246. buf = buf + s.slice(i, k)
  247. }
  248. i = k
  249. } else {
  250. buf = buf + s[i]
  251. i = i + 1
  252. }
  253. }
  254. if (buf != '') { out.push(textSpan(buf, strong, em)) }
  255. return out
  256. }
  257. // ---- blocks -------------------------------------------------------------------------------
  258. static noCr = (line) => { return line.length > 0 && line.charCodeAt(line.length - 1) == 13 ? line.slice(0, line.length - 1) : line }
  259. static indentOf = (line) => {
  260. let i = 0
  261. while (i < line.length && (line.charCodeAt(i) == 32 || line.charCodeAt(i) == 9)) { i = i + 1 }
  262. return i
  263. }
  264. // a heading line (≤ 3 spaces in, 1–6 '#', a space or the end) → { level, text } or null
  265. static headingOf = (line) => {
  266. if (indentOf(line) > 3) { return null }
  267. let t = line.trim()
  268. let k = 0
  269. while (k < t.length && t.charCodeAt(k) == 35) { k = k + 1 }
  270. if (k == 0 || k > 6) { return null }
  271. if (k < t.length && t.charCodeAt(k) != 32 && t.charCodeAt(k) != 9) { return null }
  272. let body = t.slice(k).trim()
  273. // a closing run of '#' after a space goes
  274. let e = body.length
  275. while (e > 0 && body.charCodeAt(e - 1) == 35) { e = e - 1 }
  276. 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() }
  277. return { level = k text = body }
  278. }
  279. // a list item line → { ordered, start, text } or null
  280. static itemOf = (line) => {
  281. let t = line.trim()
  282. if (t.length >= 2 && (t.charCodeAt(0) == 45 || t.charCodeAt(0) == 42 || t.charCodeAt(0) == 43) && (t.charCodeAt(1) == 32 || t.charCodeAt(1) == 9)) {
  283. return { ordered = false start = 1 text = t.slice(2).trim() }
  284. }
  285. if (t.length == 1 && (t == '-' || t == '*' || t == '+')) { return { ordered = false start = 1 text = '' } }
  286. let k = 0
  287. while (k < t.length && k < 9 && t.charCodeAt(k) >= 48 && t.charCodeAt(k) <= 57) { k = k + 1 }
  288. if (k == 0 || k >= t.length) { return null }
  289. let d = t.charCodeAt(k)
  290. if (d != 46 && d != 41) { return null }
  291. if (k + 1 < t.length && t.charCodeAt(k + 1) != 32 && t.charCodeAt(k + 1) != 9) { return null }
  292. return { ordered = true start = toNumber(t.slice(0, k)) text = t.slice(k + 1).trim() }
  293. }
  294. // a code fence line (``` or ~~~, ≥ 3) → its run (e.g. '````') or null
  295. static fenceOf = (line) => {
  296. if (indentOf(line) > 3) { return null }
  297. let t = line.trim()
  298. if (t.length < 3) { return null }
  299. let c = t.charCodeAt(0)
  300. if (c != 96 && c != 126) { return null }
  301. let k = 0
  302. while (k < t.length && t.charCodeAt(k) == c) { k = k + 1 }
  303. if (k < 3) { return null }
  304. // an info string after ``` must not contain a backtick
  305. if (c == 96 && t.slice(k).includes('`')) { return null }
  306. return t.slice(0, k)
  307. }
  308. static paraBlock = (lines) => {
  309. let spans = inlineSpans(lines.join(NL))
  310. 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 = [] }
  311. }
  312. static headingBlock = (level, text) => {
  313. let spans = inlineSpans(text)
  314. 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 = [] }
  315. }
  316. static codeBlock = (lines) => {
  317. let text = lines.join(NL)
  318. 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 = [] }
  319. }
  320. static listBlock = (ordered, first, itemLines) => {
  321. let items = []
  322. for (lines of itemLines) {
  323. let spans = inlineSpans(lines.join(NL))
  324. items.push({ spans = spans })
  325. }
  326. 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 = [] }
  327. }
  328. static quoteBlock = (lines) => {
  329. let spans = inlineSpans(lines.join(NL))
  330. 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 = [] }
  331. }
  332. static hrBlock = () => {
  333. 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 = [] }
  334. }
  335. static tableBlock = (head, rows) => {
  336. 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 }
  337. }
  338. // a line of 3+ of one of - * _ (spaces between allowed) and nothing else
  339. static isHrLine = (line) => {
  340. if (indentOf(line) > 3) { return false }
  341. let t = line.trim()
  342. if (t.length < 3) { return false }
  343. let c = t.charCodeAt(0)
  344. if (c != 45 && c != 42 && c != 95) { return false }
  345. let n = 0
  346. let i = 0
  347. while (i < t.length) {
  348. let d = t.charCodeAt(i)
  349. if (d == c) { n = n + 1 } else if (d != 32 && d != 9) { return false }
  350. i = i + 1
  351. }
  352. return n >= 3
  353. }
  354. // `> text` → the text after the marker, or null
  355. static quoteOf = (line) => {
  356. if (indentOf(line) > 3) { return null }
  357. let t = line.trim()
  358. if (!t.startsWith('>')) { return null }
  359. return t.slice(1).trim()
  360. }
  361. // the cells of a table row line: `| a | b |` → [a, b] as spans lists
  362. static cellsOf = (line) => {
  363. let t = line.trim()
  364. if (t.startsWith('|')) { t = t.slice(1) }
  365. if (t.endsWith('|')) { t = t.slice(0, t.length - 1) }
  366. let out = []
  367. for (c of t.split('|')) { out.push({ spans = inlineSpans(c.trim()) }) }
  368. return out
  369. }
  370. // is this the `|---|:--:|` line under a table head?
  371. static isSepLine = (line) => {
  372. let t = line.trim()
  373. if (!t.includes('-') || !t.includes('|') && !t.includes(':')) { return false }
  374. let i = 0
  375. while (i < t.length) {
  376. let d = t.charCodeAt(i)
  377. if (d != 124 && d != 45 && d != 58 && d != 32 && d != 9) { return false }
  378. i = i + 1
  379. }
  380. return true
  381. }
  382. // the Markdown text → blocks (see the header)
  383. static markdownBlocks = (text) => {
  384. let out = []
  385. if (text == null || text == '') { return out }
  386. let lines = ('' + text).split(NL)
  387. let para = []
  388. let inList = false
  389. let listOrdered = false
  390. let listStart = 1
  391. let items = []
  392. let blankBefore = false
  393. let i = 0
  394. while (i < lines.length) {
  395. let line = noCr(lines[i])
  396. let fence = fenceOf(line)
  397. let hr = fence == null && isHrLine(line)
  398. let q = fence == null && !hr ? quoteOf(line) : null
  399. let isTab = fence == null && !hr && q == null && line.includes('|') && i + 1 < lines.length && isSepLine(noCr(lines[i + 1])) && noCr(lines[i + 1]).includes('|')
  400. let h = fence == null && !hr && q == null && !isTab ? headingOf(line) : null
  401. let it = fence == null && !hr && q == null && !isTab && h == null ? itemOf(line) : null
  402. if (hr) {
  403. if (para.length > 0) { out.push(paraBlock(para)) }
  404. para = []
  405. if (inList) { out.push(listBlock(listOrdered, listStart, items)) }
  406. inList = false
  407. items = []
  408. out.push(hrBlock())
  409. blankBefore = false
  410. i = i + 1
  411. } else if (q != null) {
  412. if (para.length > 0) { out.push(paraBlock(para)) }
  413. para = []
  414. if (inList) { out.push(listBlock(listOrdered, listStart, items)) }
  415. inList = false
  416. items = []
  417. let ql = []
  418. let j = i
  419. let more = true
  420. while (j < lines.length && more) {
  421. let x = quoteOf(noCr(lines[j]))
  422. if (x == null) { more = false } else {
  423. ql.push(x)
  424. j = j + 1
  425. }
  426. }
  427. out.push(quoteBlock(ql))
  428. i = j
  429. blankBefore = false
  430. } else if (isTab) {
  431. if (para.length > 0) { out.push(paraBlock(para)) }
  432. para = []
  433. if (inList) { out.push(listBlock(listOrdered, listStart, items)) }
  434. inList = false
  435. items = []
  436. let head = cellsOf(line)
  437. let rows = []
  438. let j = i + 2
  439. while (j < lines.length && noCr(lines[j]).trim() != '' && noCr(lines[j]).includes('|')) {
  440. rows.push(cellsOf(noCr(lines[j])))
  441. j = j + 1
  442. }
  443. out.push(tableBlock(head, rows))
  444. i = j
  445. blankBefore = false
  446. } else if (fence != null) {
  447. if (para.length > 0) { out.push(paraBlock(para)) }
  448. para = []
  449. if (inList) { out.push(listBlock(listOrdered, listStart, items)) }
  450. inList = false
  451. items = []
  452. let code = []
  453. let j = i + 1
  454. let closed = false
  455. let fc = fence.charCodeAt(0)
  456. while (j < lines.length && !closed) {
  457. let l = noCr(lines[j])
  458. let f = fenceOf(l)
  459. if (f != null && f.charCodeAt(0) == fc && f.length >= fence.length && l.trim() == f) {
  460. closed = true
  461. } else {
  462. code.push(l)
  463. }
  464. j = j + 1
  465. }
  466. out.push(codeBlock(code))
  467. i = j
  468. blankBefore = false
  469. } else if (line.trim() == '') {
  470. if (para.length > 0) { out.push(paraBlock(para)) }
  471. para = []
  472. blankBefore = true
  473. i = i + 1
  474. } else if (h != null) {
  475. if (para.length > 0) { out.push(paraBlock(para)) }
  476. para = []
  477. if (inList) { out.push(listBlock(listOrdered, listStart, items)) }
  478. inList = false
  479. items = []
  480. out.push(headingBlock(h.level, h.text))
  481. blankBefore = false
  482. i = i + 1
  483. } else if (it != null) {
  484. if (para.length > 0) { out.push(paraBlock(para)) }
  485. para = []
  486. if (inList && listOrdered != it.ordered) {
  487. out.push(listBlock(listOrdered, listStart, items))
  488. inList = false
  489. items = []
  490. }
  491. if (!inList) {
  492. inList = true
  493. listOrdered = it.ordered
  494. listStart = it.start
  495. items = []
  496. }
  497. items.push([it.text])
  498. blankBefore = false
  499. i = i + 1
  500. } else if (inList && (!blankBefore || indentOf(line) >= 2)) {
  501. // a wrapped item line (or an indented paragraph of the item): onto the last item
  502. let last = items[items.length - 1]
  503. last.push(line.trim())
  504. items[items.length - 1] = last
  505. blankBefore = false
  506. i = i + 1
  507. } else {
  508. if (inList) { out.push(listBlock(listOrdered, listStart, items)) }
  509. inList = false
  510. items = []
  511. para.push(line.trim())
  512. blankBefore = false
  513. i = i + 1
  514. }
  515. }
  516. if (para.length > 0) { out.push(paraBlock(para)) }
  517. if (inList) { out.push(listBlock(listOrdered, listStart, items)) }
  518. return out
  519. }

Branches

Latest commits

  • 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