gitoriaLog in with ident

ident

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Commitd2e7f91bd2e7f91bident mission 009 (2/4): one lib file per topic (login, accounts, identities, apps, invites, selector, notify + helpers, util), function routes as thin wrappers in lib/api.hl, project.hl = the map; same outputmred2e7f91b/lib/notify-helpers.hl

4.6 KB

  1. // lib/notify-helpers.hl — what lib/notify.hl looks up: the checks of a kind name, a text and a URL, the time
  2. // stamp, the two orders (newest first, by name), on/off words. No table access. Statics only.
  3. import { timestamp } from 'hl:time'
  4. import { countOf, hasControl, urlChars } from './util.hl'
  5. static maxName = 60
  6. static maxText = 2000
  7. static maxUrl = 2000
  8. static bool = (v) => { return v == true }
  9. static onOff = (b) => { return b ? 'on' : 'off' }
  10. // "YYYY-MM-DD HH:MM UTC" (hl:time knows no zones, hybriel #11)
  11. static stamp = (ms) => {
  12. if (ms == null) { return '' }
  13. let t = timestamp(ms)
  14. return t.slice(0, 10) + ' ' + t.slice(11, 16) + ' UTC'
  15. }
  16. // NEWEST FIRST by `created` (a tie by the id); insertion sort by hand (no list sort(), hybriel #6)
  17. static idAfter = (a, b) => {
  18. let i = 0
  19. while (i < a.length && i < b.length) {
  20. let x = a.charCodeAt(i)
  21. let y = b.charCodeAt(i)
  22. if (x != y) { return x > y }
  23. i = i + 1
  24. }
  25. return a.length > b.length
  26. }
  27. static newerThan = (a, b) => {
  28. let ca = a.created == null ? 0 : a.created
  29. let cb = b.created == null ? 0 : b.created
  30. if (ca != cb) { return ca > cb }
  31. return idAfter(a.id, b.id)
  32. }
  33. static newestFirst = (list) => {
  34. let out = []
  35. if (countOf(list) == 0) { return out }
  36. for (x of list) { out.push(x) }
  37. let i = 1
  38. while (i < out.length) {
  39. let cur = out[i]
  40. let j = i - 1
  41. while (j >= 0 && newerThan(cur, out[j])) {
  42. out[j + 1] = out[j]
  43. j = j - 1
  44. }
  45. out[j + 1] = cur
  46. i = i + 1
  47. }
  48. return out
  49. }
  50. // kinds by name, A-Z (char codes)
  51. static nameBefore = (a, b) => {
  52. let x = a.name.toLowerCase()
  53. let y = b.name.toLowerCase()
  54. let i = 0
  55. while (i < x.length && i < y.length) {
  56. if (x.charCodeAt(i) != y.charCodeAt(i)) { return x.charCodeAt(i) < y.charCodeAt(i) }
  57. i = i + 1
  58. }
  59. return x.length < y.length
  60. }
  61. static byName = (list) => {
  62. let out = []
  63. if (countOf(list) == 0) { return out }
  64. for (x of list) { out.push(x) }
  65. let i = 1
  66. while (i < out.length) {
  67. let cur = out[i]
  68. let j = i - 1
  69. while (j >= 0 && nameBefore(cur, out[j])) {
  70. out[j + 1] = out[j]
  71. j = j - 1
  72. }
  73. out[j + 1] = cur
  74. i = i + 1
  75. }
  76. return out
  77. }
  78. // ---- validation -------------------------------------------------------------------------
  79. // A KIND NAME / NOTIFICATION NAME: trimmed, 1-60 chars, one line. Answers { name } or { error }.
  80. static checkKindName = (v, what) => {
  81. if (v == null || hlTypeName(v) != 'String') { return { error = 'field ' + what + ' must be a string' } }
  82. let t = v.trim()
  83. if (t == '') { return { error = 'field ' + what + ' is empty' } }
  84. if (t.length > maxName) { return { error = 'field ' + what + ' is longer than ' + maxName + ' characters' } }
  85. if (hasControl(t)) { return { error = 'field ' + what + ' contains a control character' } }
  86. return { name = t }
  87. }
  88. // THE TEXT: 1-2000 chars (Markdown by the worldapi convention; shown as plain text with
  89. // its line breaks); tab, line feed and carriage return are the only control characters
  90. static checkText = (v) => {
  91. if (v == null || hlTypeName(v) != 'String') { return { error = 'field text must be a string' } }
  92. let t = v.trim()
  93. if (t == '') { return { error = 'field text is empty' } }
  94. if (t.length > maxText) { return { error = 'field text is longer than ' + maxText + ' characters' } }
  95. let i = 0
  96. while (i < t.length) {
  97. let c = t.charCodeAt(i)
  98. if ((c < 32 && c != 9 && c != 10 && c != 13) || c == 127) { return { error = 'field text contains a control character' } }
  99. i = i + 1
  100. }
  101. return { text = t }
  102. }
  103. // AN ICON / ACTION LINK: an absolute http(s) URL, printable URL characters only, ≤ 2000
  104. static checkUrl = (v, what) => {
  105. if (v == null || hlTypeName(v) != 'String') { return { error = 'field ' + what + ' must be a string' } }
  106. let t = v.trim()
  107. if (t.length > maxUrl) { return { error = 'field ' + what + ' is longer than ' + maxUrl + ' characters' } }
  108. let i = 0
  109. while (i < t.length) {
  110. if (!urlChars.includes(t[i])) { return { error = 'field ' + what + ' contains a character that is not allowed in a URL' } }
  111. i = i + 1
  112. }
  113. let lower = t.toLowerCase()
  114. let scheme = lower.slice(0, 7) == 'http://' ? 7 : (lower.slice(0, 8) == 'https://' ? 8 : 0)
  115. if (scheme == 0) { return { error = 'field ' + what + ' must be an http:// or https:// URL' } }
  116. let rest = t.slice(scheme, t.length)
  117. if (rest == '' || rest[0] == '/' || rest[0] == '?') { return { error = 'field ' + what + ' has no host' } }
  118. return { url = t }
  119. }
  120. // a user's switch over the app's preset: 'on' / 'off', or '' = the preset
  121. static switchValue = (userValue, preset) => {
  122. if (userValue == 'on') { return true }
  123. if (userValue == 'off') { return false }
  124. return preset
  125. }
  126. static kindRowOut = (k) => { return { name = k.name push = bool(k.push) email = bool(k.email) } }
  127. static unreadCount = (rows) => {
  128. let n = 0
  129. for (r of rows) { if (r.unread) { n = n + 1 } }
  130. return n
  131. }

Branches

Latest commits

  • d2e7f91bident mission 009 (2/4): one lib file per topic (login, accounts, identities, apps, invites, selector, notify + helpers, util), function routes as thin wrappers in lib/api.hl, project.hl = the map; same outputmre
  • 91017164ident mission 009 (1/4): file moves — the root .hl files into lib/ (api.hl → lib/api-helpers.hl), styles.hl → components/styles.hl; imports adjusted, no other changemre
  • f8bdcbc2ident: Hybriel master 06617221 (plugin allocators 3a781359 + 413f60e4, mpackdb 2cb7ae5e, http1 773de63e); all gates greenmre
  • ff78726cident: Hybriel master 190aa11d (fc838894 GC correctness, #127, #126 closure scopes); gates all greenmre
  • a3a7d21aident: Hybriel master 8efba065 (#126 GC by bytes, #48 lambda params copy); session-writing lambdas take &sessionmre
  • 98226b41antcolony#40: mission references point to the moved missionsmre
  • ff805b9aantcolony#40: history (LOG.md), worker briefs (missions/) and reports moved here from antcolony, numbered per project; old numbers in antcolony docs/mission-map.mdmre
  • 51a7bcdfident: Hybriel master 73267707 (#122); /code uses the new page() signature; pending address passed as parameter; once-checksmre
  • 836f644fident#24: installable app (manifest, service worker, data-free offline /start), own iconmre
  • 8bebbbf2deploy.sh: back up live storage/.sessions/.env before every deploy (newest 5 kept)mre
  • cc063ea2deploy.sh: never send .git or .gitignore to Byrodinmre
  • 81b15b7bState of 2026-09-27, before the move to gitoriamre