@jant/core 0.6.15 → 0.6.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/bin/commands/import-site.js +7 -0
  2. package/dist/app-C75Zc8dD.js +6 -0
  3. package/dist/{app-C-oi_t8W.js → app-C7_4oab9.js} +480 -196
  4. package/dist/client/.vite/manifest.json +8 -8
  5. package/dist/client/_assets/chunks/{url-pLre2DM_.js → url-CU9pEbk5.js} +1 -1
  6. package/dist/client/_assets/{client-DxY5BFe8.js → client-Bq0vre8Y.js} +3 -3
  7. package/dist/client/_assets/client-D3VZyG4L.css +2 -0
  8. package/dist/client/_assets/{client-auth-CakPESv9.js → client-auth-F2rb9WfF.js} +1015 -977
  9. package/dist/{env-OHRKGcMj.js → env-BPYViDJJ.js} +1 -1
  10. package/dist/{export-CsFx6F_M.js → export-Dl5KCiEs.js} +93 -7
  11. package/dist/{github-api-BgSiE71w.js → github-api-BNF35Lkx.js} +1 -1
  12. package/dist/{github-app-BbklkFmU.js → github-app-DdiD-bC4.js} +1 -1
  13. package/dist/{github-sync-ppWXN3jb.js → github-sync-Bqg62IvV.js} +3 -3
  14. package/dist/{github-sync-Cq1pzzOI.js → github-sync-HdK2EgvJ.js} +3 -3
  15. package/dist/index.js +5 -5
  16. package/dist/node.js +6 -6
  17. package/dist/{url-BMYO-Zlt.js → url-CbLAtlZe.js} +517 -12
  18. package/package.json +1 -1
  19. package/src/__tests__/mise-config.test.ts +22 -0
  20. package/src/client/__tests__/compose-bridge.test.ts +252 -2
  21. package/src/client/__tests__/compose-shortcuts.test.ts +20 -0
  22. package/src/client/__tests__/toast.test.ts +65 -1
  23. package/src/client/components/__tests__/compose-format-convert.test.ts +0 -19
  24. package/src/client/components/__tests__/jant-command-palette.test.ts +51 -0
  25. package/src/client/components/__tests__/jant-compose-dialog.test.ts +2015 -318
  26. package/src/client/components/__tests__/jant-compose-editor.test.ts +53 -62
  27. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +35 -89
  28. package/src/client/components/compose-format-convert.ts +5 -1
  29. package/src/client/components/compose-types.ts +47 -3
  30. package/src/client/components/jant-command-palette.ts +12 -6
  31. package/src/client/components/jant-compose-dialog.ts +1464 -680
  32. package/src/client/components/jant-compose-editor.ts +225 -93
  33. package/src/client/components/jant-compose-fullscreen.ts +3 -5
  34. package/src/client/components/jant-post-menu.ts +91 -21
  35. package/src/client/compose-bridge.ts +143 -122
  36. package/src/client/compose-launch.ts +13 -0
  37. package/src/client/compose-shortcuts.ts +5 -1
  38. package/src/client/post-refresh.ts +135 -0
  39. package/src/client/tiptap/__tests__/link-input-rules.test.ts +121 -0
  40. package/src/client/tiptap/link-input-rules.ts +4 -4
  41. package/src/client/toast.ts +130 -38
  42. package/src/db/__tests__/backfill-thread-activity.test.ts +234 -0
  43. package/src/db/__tests__/thread-activity.test.ts +113 -0
  44. package/src/db/backfills/0005_split_thread_activity_from_quiet_replies.sql +68 -0
  45. package/src/db/migrations/0031_many_ego.sql +3 -0
  46. package/src/db/migrations/meta/0031_snapshot.json +2562 -0
  47. package/src/db/migrations/meta/_journal.json +7 -0
  48. package/src/db/migrations/pg/0029_rare_hairball.sql +3 -0
  49. package/src/db/migrations/pg/meta/0029_snapshot.json +3289 -0
  50. package/src/db/migrations/pg/meta/_journal.json +7 -0
  51. package/src/db/pg/schema.ts +18 -0
  52. package/src/db/schema.ts +20 -0
  53. package/src/db/thread-activity.ts +101 -0
  54. package/src/i18n/locales/public/en.po +89 -21
  55. package/src/i18n/locales/public/en.ts +1 -1
  56. package/src/i18n/locales/public/zh-Hans.po +88 -20
  57. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  58. package/src/i18n/locales/public/zh-Hant.po +88 -20
  59. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  60. package/src/lib/__tests__/feed.test.ts +168 -0
  61. package/src/lib/__tests__/markdown.test.ts +16 -0
  62. package/src/lib/__tests__/slug.test.ts +18 -0
  63. package/src/lib/__tests__/translit.test.ts +87 -0
  64. package/src/lib/__tests__/url.test.ts +53 -0
  65. package/src/lib/feed.ts +113 -11
  66. package/src/lib/hugo-markdown.ts +5 -0
  67. package/src/lib/link-preview.ts +25 -0
  68. package/src/lib/markdown-manager.ts +6 -1
  69. package/src/lib/post-display.ts +42 -9
  70. package/src/lib/timeline.ts +39 -9
  71. package/src/lib/tiptap-render.ts +4 -2
  72. package/src/lib/translit.ts +288 -0
  73. package/src/lib/url.ts +123 -12
  74. package/src/lib/view.ts +28 -0
  75. package/src/routes/api/__tests__/palette.test.ts +44 -0
  76. package/src/routes/api/posts.ts +13 -9
  77. package/src/routes/api/public/__tests__/posts.test.ts +38 -0
  78. package/src/routes/api/public/posts.ts +7 -0
  79. package/src/routes/compose.tsx +6 -2
  80. package/src/routes/feed/__tests__/sitemap.test.ts +68 -1
  81. package/src/routes/pages/__tests__/archive-params.test.ts +292 -0
  82. package/src/routes/pages/__tests__/featured.test.ts +6 -2
  83. package/src/routes/pages/__tests__/preview.test.ts +3 -1
  84. package/src/routes/pages/archive.tsx +65 -17
  85. package/src/routes/pages/page.tsx +5 -2
  86. package/src/services/__tests__/collection.test.ts +32 -1
  87. package/src/services/__tests__/post-timeline.test.ts +142 -11
  88. package/src/services/__tests__/post.test.ts +277 -0
  89. package/src/services/collection.ts +8 -10
  90. package/src/services/export-theme/assets/client-site.css +1 -1
  91. package/src/services/export.ts +3 -5
  92. package/src/services/path.ts +4 -1
  93. package/src/services/post.ts +297 -100
  94. package/src/services/search.ts +9 -0
  95. package/src/styles/components.css +0 -14
  96. package/src/styles/site-media.css +4 -4
  97. package/src/styles/tokens.css +62 -1
  98. package/src/styles/ui.css +1363 -1283
  99. package/src/types/entities.ts +5 -0
  100. package/src/types/props.ts +12 -0
  101. package/src/types/views.ts +6 -0
  102. package/src/ui/compose/ComposeDialog.tsx +17 -17
  103. package/src/ui/feed/LinkPreview.tsx +2 -7
  104. package/src/ui/feed/PostStatusBadges.tsx +42 -3
  105. package/src/ui/pages/ArchivePage.tsx +131 -24
  106. package/src/ui/pages/__tests__/ArchivePage.test.tsx +63 -0
  107. package/src/ui/shared/DraftPreviewBar.tsx +25 -9
  108. package/src/ui/shared/PostFooter.tsx +35 -12
  109. package/src/ui/shared/custom-icons.ts +5 -0
  110. package/src/ui/shared/post-article-attributes.ts +6 -0
  111. package/dist/app-BxCOR3Uc.js +0 -6
  112. package/dist/client/_assets/client-B_eGKN5p.css +0 -2
@@ -11,6 +11,429 @@ var __exportAll = (all, no_symbols) => {
11
11
  return target;
12
12
  };
13
13
  //#endregion
14
+ //#region src/lib/translit.ts
15
+ /**
16
+ * Dictionary-free transliteration helpers for slug generation.
17
+ *
18
+ * These converters cover the scripts limax handles poorly:
19
+ *
20
+ * - Korean hangul → Revised Romanization (limax strips hangul entirely)
21
+ * - Japanese kana → Hepburn romaji (limax mangles long vowels and digraphs)
22
+ *
23
+ * Both are pure algorithms over Unicode code points — no dictionaries — so
24
+ * they stay small enough for the Workers bundle and the lazy client chunk.
25
+ * Japanese kanji is intentionally out of scope: readings are context-dependent
26
+ * and need a morphological dictionary, which is why `slugify` falls back to a
27
+ * random ID for kanji-heavy Japanese titles instead of guessing.
28
+ */ /** Matches hiragana, katakana, and the chōonpu (ー) long-vowel mark. */ var KANA_RE = /[ぁ-ゖァ-ヺー]/u;
29
+ /** Matches precomposed hangul syllables and compatibility jamo. */ var HANGUL_RE = /[가-힣ㄱ-ㆎ]/u;
30
+ /** Matches Han (CJK ideograph) characters. */ var HAN_RE = /\p{Script=Han}/u;
31
+ var HANGUL_SYLLABLE_BASE = 44032;
32
+ var HANGUL_SYLLABLE_END = 55203;
33
+ var VOWEL_COUNT = 21;
34
+ var FINAL_COUNT = 28;
35
+ var RR_INITIALS = [
36
+ "g",
37
+ "kk",
38
+ "n",
39
+ "d",
40
+ "tt",
41
+ "r",
42
+ "m",
43
+ "b",
44
+ "pp",
45
+ "s",
46
+ "ss",
47
+ "",
48
+ "j",
49
+ "jj",
50
+ "ch",
51
+ "k",
52
+ "t",
53
+ "p",
54
+ "h"
55
+ ];
56
+ var RR_VOWELS = [
57
+ "a",
58
+ "ae",
59
+ "ya",
60
+ "yae",
61
+ "eo",
62
+ "e",
63
+ "yeo",
64
+ "ye",
65
+ "o",
66
+ "wa",
67
+ "wae",
68
+ "oe",
69
+ "yo",
70
+ "u",
71
+ "wo",
72
+ "we",
73
+ "wi",
74
+ "yu",
75
+ "eu",
76
+ "ui",
77
+ "i"
78
+ ];
79
+ var RR_FINALS = [
80
+ "",
81
+ "k",
82
+ "k",
83
+ "k",
84
+ "n",
85
+ "n",
86
+ "n",
87
+ "t",
88
+ "l",
89
+ "k",
90
+ "m",
91
+ "l",
92
+ "l",
93
+ "l",
94
+ "p",
95
+ "l",
96
+ "m",
97
+ "p",
98
+ "p",
99
+ "t",
100
+ "t",
101
+ "ng",
102
+ "t",
103
+ "t",
104
+ "k",
105
+ "t",
106
+ "p",
107
+ "t"
108
+ ];
109
+ var RR_LIAISON = [
110
+ ["", ""],
111
+ ["", "g"],
112
+ ["", "kk"],
113
+ ["k", "s"],
114
+ ["", "n"],
115
+ ["n", "j"],
116
+ ["n", ""],
117
+ ["", "d"],
118
+ ["", "r"],
119
+ ["l", "g"],
120
+ ["l", "m"],
121
+ ["l", "b"],
122
+ ["l", "s"],
123
+ ["l", "t"],
124
+ ["l", "p"],
125
+ ["l", ""],
126
+ ["", "m"],
127
+ ["", "b"],
128
+ ["p", "s"],
129
+ ["", "s"],
130
+ ["", "ss"],
131
+ ["ng", ""],
132
+ ["", "j"],
133
+ ["", "ch"],
134
+ ["", "k"],
135
+ ["", "t"],
136
+ ["", "p"],
137
+ ["", ""]
138
+ ];
139
+ var COMPAT_JAMO = {
140
+ ㄱ: "g",
141
+ ㄲ: "kk",
142
+ ㄴ: "n",
143
+ ㄷ: "d",
144
+ ㄸ: "tt",
145
+ ㄹ: "r",
146
+ ㅁ: "m",
147
+ ㅂ: "b",
148
+ ㅃ: "pp",
149
+ ㅅ: "s",
150
+ ㅆ: "ss",
151
+ ㅇ: "ng",
152
+ ㅈ: "j",
153
+ ㅉ: "jj",
154
+ ㅊ: "ch",
155
+ ㅋ: "k",
156
+ ㅌ: "t",
157
+ ㅍ: "p",
158
+ ㅎ: "h",
159
+ ㅏ: "a",
160
+ ㅐ: "ae",
161
+ ㅑ: "ya",
162
+ ㅒ: "yae",
163
+ ㅓ: "eo",
164
+ ㅔ: "e",
165
+ ㅕ: "yeo",
166
+ ㅖ: "ye",
167
+ ㅗ: "o",
168
+ ㅘ: "wa",
169
+ ㅙ: "wae",
170
+ ㅚ: "oe",
171
+ ㅛ: "yo",
172
+ ㅜ: "u",
173
+ ㅝ: "wo",
174
+ ㅞ: "we",
175
+ ㅟ: "wi",
176
+ ㅠ: "yu",
177
+ ㅡ: "eu",
178
+ ㅢ: "ui",
179
+ ㅣ: "i"
180
+ };
181
+ function decomposeHangul(codePoint) {
182
+ if (codePoint < HANGUL_SYLLABLE_BASE || codePoint > HANGUL_SYLLABLE_END) return null;
183
+ const index = codePoint - HANGUL_SYLLABLE_BASE;
184
+ return {
185
+ initial: Math.floor(index / (VOWEL_COUNT * FINAL_COUNT)),
186
+ vowel: Math.floor(index / FINAL_COUNT) % VOWEL_COUNT,
187
+ final: index % FINAL_COUNT
188
+ };
189
+ }
190
+ /**
191
+ * Transliterates Korean hangul to Latin using Revised Romanization.
192
+ *
193
+ * Handles precomposed syllables with basic liaison (a final consonant before
194
+ * a silent-ㅇ syllable carries over as its initial) and standalone
195
+ * compatibility jamo. Assimilation sound rules (e.g. 종로 → "Jongno") are
196
+ * intentionally skipped — slugs need recognizable output, not phonetic
197
+ * perfection. Non-hangul characters pass through unchanged.
198
+ *
199
+ * @param text - Input text, possibly mixing hangul with other characters
200
+ * @returns Text with hangul replaced by its romanization
201
+ *
202
+ * @example
203
+ * ```ts
204
+ * hangulToLatin("안녕하세요"); // "annyeonghaseyo"
205
+ * hangulToLatin("한국어"); // "hangugeo" (liaison)
206
+ * hangulToLatin("개발 일지"); // "gaebal ilji"
207
+ * ```
208
+ */ function hangulToLatin(text) {
209
+ const chars = [...text];
210
+ let out = "";
211
+ let carried = null;
212
+ for (const [i, char] of chars.entries()) {
213
+ const syllable = decomposeHangul(char.codePointAt(0) ?? 0);
214
+ if (!syllable) {
215
+ carried = null;
216
+ out += COMPAT_JAMO[char] ?? char;
217
+ continue;
218
+ }
219
+ const initial = carried ?? RR_INITIALS[syllable.initial] ?? "";
220
+ carried = null;
221
+ const next = decomposeHangul(chars[i + 1]?.codePointAt(0) ?? 0);
222
+ let final = RR_FINALS[syllable.final] ?? "";
223
+ if (syllable.final !== 0 && next && RR_INITIALS[next.initial] === "") {
224
+ const [remains, carry] = RR_LIAISON[syllable.final] ?? ["", ""];
225
+ final = remains;
226
+ carried = carry;
227
+ }
228
+ out += initial + (RR_VOWELS[syllable.vowel] ?? "") + final;
229
+ }
230
+ return out;
231
+ }
232
+ var KANA_BASE = {
233
+ あ: "a",
234
+ い: "i",
235
+ う: "u",
236
+ え: "e",
237
+ お: "o",
238
+ か: "ka",
239
+ き: "ki",
240
+ く: "ku",
241
+ け: "ke",
242
+ こ: "ko",
243
+ さ: "sa",
244
+ し: "shi",
245
+ す: "su",
246
+ せ: "se",
247
+ そ: "so",
248
+ た: "ta",
249
+ ち: "chi",
250
+ つ: "tsu",
251
+ て: "te",
252
+ と: "to",
253
+ な: "na",
254
+ に: "ni",
255
+ ぬ: "nu",
256
+ ね: "ne",
257
+ の: "no",
258
+ は: "ha",
259
+ ひ: "hi",
260
+ ふ: "fu",
261
+ へ: "he",
262
+ ほ: "ho",
263
+ ま: "ma",
264
+ み: "mi",
265
+ む: "mu",
266
+ め: "me",
267
+ も: "mo",
268
+ や: "ya",
269
+ ゆ: "yu",
270
+ よ: "yo",
271
+ ら: "ra",
272
+ り: "ri",
273
+ る: "ru",
274
+ れ: "re",
275
+ ろ: "ro",
276
+ わ: "wa",
277
+ ゐ: "i",
278
+ ゑ: "e",
279
+ を: "o",
280
+ ん: "n",
281
+ が: "ga",
282
+ ぎ: "gi",
283
+ ぐ: "gu",
284
+ げ: "ge",
285
+ ご: "go",
286
+ ざ: "za",
287
+ じ: "ji",
288
+ ず: "zu",
289
+ ぜ: "ze",
290
+ ぞ: "zo",
291
+ だ: "da",
292
+ ぢ: "ji",
293
+ づ: "zu",
294
+ で: "de",
295
+ ど: "do",
296
+ ば: "ba",
297
+ び: "bi",
298
+ ぶ: "bu",
299
+ べ: "be",
300
+ ぼ: "bo",
301
+ ぱ: "pa",
302
+ ぴ: "pi",
303
+ ぷ: "pu",
304
+ ぺ: "pe",
305
+ ぽ: "po",
306
+ ゔ: "vu",
307
+ ぁ: "a",
308
+ ぃ: "i",
309
+ ぅ: "u",
310
+ ぇ: "e",
311
+ ぉ: "o",
312
+ ゃ: "ya",
313
+ ゅ: "yu",
314
+ ょ: "yo",
315
+ ゎ: "wa"
316
+ };
317
+ var KANA_COMBO = {
318
+ きゃ: "kya",
319
+ きゅ: "kyu",
320
+ きょ: "kyo",
321
+ ぎゃ: "gya",
322
+ ぎゅ: "gyu",
323
+ ぎょ: "gyo",
324
+ しゃ: "sha",
325
+ しゅ: "shu",
326
+ しょ: "sho",
327
+ じゃ: "ja",
328
+ じゅ: "ju",
329
+ じょ: "jo",
330
+ ちゃ: "cha",
331
+ ちゅ: "chu",
332
+ ちょ: "cho",
333
+ ぢゃ: "ja",
334
+ ぢゅ: "ju",
335
+ ぢょ: "jo",
336
+ にゃ: "nya",
337
+ にゅ: "nyu",
338
+ にょ: "nyo",
339
+ ひゃ: "hya",
340
+ ひゅ: "hyu",
341
+ ひょ: "hyo",
342
+ びゃ: "bya",
343
+ びゅ: "byu",
344
+ びょ: "byo",
345
+ ぴゃ: "pya",
346
+ ぴゅ: "pyu",
347
+ ぴょ: "pyo",
348
+ みゃ: "mya",
349
+ みゅ: "myu",
350
+ みょ: "myo",
351
+ りゃ: "rya",
352
+ りゅ: "ryu",
353
+ りょ: "ryo",
354
+ ふぁ: "fa",
355
+ ふぃ: "fi",
356
+ ふぇ: "fe",
357
+ ふぉ: "fo",
358
+ てぃ: "ti",
359
+ でぃ: "di",
360
+ とぅ: "tu",
361
+ どぅ: "du",
362
+ うぃ: "wi",
363
+ うぇ: "we",
364
+ うぉ: "wo",
365
+ ゔぁ: "va",
366
+ ゔぃ: "vi",
367
+ ゔぇ: "ve",
368
+ ゔぉ: "vo",
369
+ ちぇ: "che",
370
+ しぇ: "she",
371
+ じぇ: "je",
372
+ つぁ: "tsa",
373
+ つぃ: "tsi",
374
+ つぇ: "tse",
375
+ つぉ: "tso",
376
+ いぇ: "ye"
377
+ };
378
+ var KATAKANA_START = 12449;
379
+ var KATAKANA_END = 12534;
380
+ var KATAKANA_TO_HIRAGANA_SHIFT = 96;
381
+ var SOKUON = "っ";
382
+ var CHOONPU = "ー";
383
+ function normalizeToHiragana(char) {
384
+ const codePoint = char.codePointAt(0) ?? 0;
385
+ if (codePoint >= KATAKANA_START && codePoint <= KATAKANA_END) return String.fromCodePoint(codePoint - KATAKANA_TO_HIRAGANA_SHIFT);
386
+ return char;
387
+ }
388
+ /**
389
+ * Transliterates Japanese kana to macron-less Hepburn romaji.
390
+ *
391
+ * Handles hiragana and katakana, digraphs (きゃ → "kya"), extended katakana
392
+ * loan combos (ファ → "fa"), sokuon gemination (まっちゃ → "matcha"), and
393
+ * drops the chōonpu long-vowel mark (ハンバーガー → "hanbaga"), matching how
394
+ * macrons are conventionally stripped in URLs. Kanji and other non-kana
395
+ * characters pass through unchanged.
396
+ *
397
+ * @param text - Input text, possibly mixing kana with other characters
398
+ * @returns Text with kana replaced by romaji
399
+ *
400
+ * @example
401
+ * ```ts
402
+ * kanaToRomaji("カタカナタイトル"); // "katakanataitoru"
403
+ * kanaToRomaji("ハンバーガー"); // "hanbaga"
404
+ * kanaToRomaji("まっちゃ"); // "matcha"
405
+ * ```
406
+ */ function kanaToRomaji(text) {
407
+ const chars = [...text].map(normalizeToHiragana);
408
+ let out = "";
409
+ let geminate = false;
410
+ for (let i = 0; i < chars.length; i++) {
411
+ const char = chars[i] ?? "";
412
+ if (char === SOKUON) {
413
+ geminate = true;
414
+ continue;
415
+ }
416
+ if (char === CHOONPU) {
417
+ geminate = false;
418
+ continue;
419
+ }
420
+ const combo = KANA_COMBO[char + (chars[i + 1] ?? "")];
421
+ const romaji = combo ?? KANA_BASE[char];
422
+ if (combo) i++;
423
+ if (romaji === void 0) {
424
+ geminate = false;
425
+ out += char;
426
+ continue;
427
+ }
428
+ if (geminate) {
429
+ geminate = false;
430
+ if (/^[^aeiou]/.test(romaji)) out += romaji.startsWith("ch") ? "t" : romaji[0] ?? "";
431
+ }
432
+ out += romaji;
433
+ }
434
+ return out;
435
+ }
436
+ //#endregion
14
437
  //#region src/lib/url.ts
15
438
  /**
16
439
  * URL Utilities
@@ -26,6 +449,7 @@ var __exportAll = (all, no_symbols) => {
26
449
  normalizePath: () => normalizePath,
27
450
  normalizeSitePathPrefix: () => normalizeSitePathPrefix,
28
451
  normalizeSiteUrl: () => normalizeSiteUrl,
452
+ sanitizeRichTextHref: () => sanitizeRichTextHref,
29
453
  sanitizeUrl: () => sanitizeUrl,
30
454
  slugify: () => slugify,
31
455
  stripSitePathPrefix: () => stripSitePathPrefix,
@@ -148,14 +572,54 @@ function normalizeSitePathname(pathname) {
148
572
  if (parsed.hostname !== reference.hostname) return null;
149
573
  return `${parsed.pathname}${parsed.search}${parsed.hash}` || "/";
150
574
  }
575
+ var HAN_GLOBAL_RE = /\p{Script=Han}/gu;
576
+ var CONTENT_CHAR_RE = /[\p{L}\p{N}]/u;
577
+ var ASCII_ALNUM_RE = /[a-zA-Z0-9]/;
578
+ /**
579
+ * Minimum fraction of a title's letters/digits the transliteration pipeline
580
+ * must handle for the result to be usable. Below this, too much meaning is
581
+ * lost (e.g. mixed kanji+kana Japanese where the semantic core is kanji, or
582
+ * scripts limax drops entirely) and `slugify` returns "" so callers fall back
583
+ * to their random-ID path instead of shipping a garbled slug.
584
+ */ var SLUG_COVERAGE_THRESHOLD = .75;
585
+ /** Per-character limax probe cache: does this char transliterate to anything? */ var charCoverageCache = /* @__PURE__ */ new Map();
586
+ function isCharCovered(char) {
587
+ if (ASCII_ALNUM_RE.test(char)) return true;
588
+ let covered = charCoverageCache.get(char);
589
+ if (covered === void 0) {
590
+ covered = limax(char, { tone: false }).length > 0;
591
+ charCoverageCache.set(char, covered);
592
+ }
593
+ return covered;
594
+ }
595
+ function slugCoverage(text, japanese) {
596
+ let content = 0;
597
+ let covered = 0;
598
+ for (const char of text) {
599
+ if (!CONTENT_CHAR_RE.test(char)) continue;
600
+ content++;
601
+ if (KANA_RE.test(char) || HANGUL_RE.test(char)) covered++;
602
+ else if (japanese && HAN_RE.test(char)) {} else if (isCharCovered(char)) covered++;
603
+ }
604
+ return content === 0 ? 0 : covered / content;
605
+ }
151
606
  /**
152
607
  * Converts text to a URL-friendly slug.
153
608
  *
154
- * Transforms text into a lowercase, hyphen-separated slug using limax for
155
- * i18n-aware transliteration (CJK → Pinyin, Japanese → Romaji, accented → ASCII).
609
+ * Transliterates i18n text to Latin before slugifying: Korean hangul via
610
+ * Revised Romanization, Japanese kana via Hepburn romaji (both in-house, see
611
+ * `translit.ts`), everything else (Han → pinyin, Cyrillic, accents, …) via
612
+ * limax. When the title contains kana it is treated as Japanese and Han
613
+ * characters are dropped rather than misread as Chinese pinyin; Han without
614
+ * kana is treated as Chinese.
615
+ *
616
+ * Returns `""` when transliteration would lose too much of the title (see
617
+ * `SLUG_COVERAGE_THRESHOLD`) — every caller treats an empty base as "no
618
+ * usable title" and falls back to a random ID, which beats publishing a
619
+ * garbled slug.
156
620
  *
157
621
  * @param text - The text to convert to a slug
158
- * @returns The slugified string
622
+ * @returns The slugified string, or `""` when no usable slug can be derived
159
623
  *
160
624
  * @example
161
625
  * ```ts
@@ -164,9 +628,24 @@ function normalizeSitePathname(pathname) {
164
628
  *
165
629
  * slugify("书评");
166
630
  * // Returns: "shu-ping"
631
+ *
632
+ * slugify("안녕하세요 세계");
633
+ * // Returns: "annyeonghaseyo-segye"
634
+ *
635
+ * slugify("ハンバーガー");
636
+ * // Returns: "hanbaga"
637
+ *
638
+ * slugify("日本語のタイトルです");
639
+ * // Returns: "" (kanji carries the meaning and can't be transliterated)
167
640
  * ```
168
641
  */ function slugify(text) {
169
- return limax(text, { tone: false }).replace(/_/g, "-");
642
+ const japanese = KANA_RE.test(text);
643
+ let pre = text;
644
+ if (japanese) pre = kanaToRomaji(pre.replace(HAN_GLOBAL_RE, " "));
645
+ if (HANGUL_RE.test(pre)) pre = hangulToLatin(pre);
646
+ const base = limax(pre, { tone: false }).replace(/_/g, "-");
647
+ if (!base) return "";
648
+ return slugCoverage(text, japanese) >= SLUG_COVERAGE_THRESHOLD ? base : "";
170
649
  }
171
650
  /**
172
651
  * Extracts a human-friendly domain name from a URL for display purposes.
@@ -198,6 +677,20 @@ var SAFE_URL_PROTOCOLS = new Set([
198
677
  "https:",
199
678
  "mailto:"
200
679
  ]);
680
+ var SAFE_RICH_TEXT_HREF_PROTOCOLS = new Set([
681
+ ...SAFE_URL_PROTOCOLS,
682
+ "tel:",
683
+ "sms:"
684
+ ]);
685
+ function sanitizeUrlWithProtocols(url, protocols) {
686
+ try {
687
+ const parsed = new URL(url, "https://placeholder.invalid");
688
+ if (protocols.has(parsed.protocol)) return url;
689
+ return "";
690
+ } catch {
691
+ return "";
692
+ }
693
+ }
201
694
  /**
202
695
  * Checks whether a string is a safe absolute URL.
203
696
  *
@@ -241,13 +734,25 @@ var SAFE_URL_PROTOCOLS = new Set([
241
734
  * sanitizeUrl("data:text/html,<h1>Hi</h1>"); // ""
242
735
  * ```
243
736
  */ function sanitizeUrl(url) {
244
- try {
245
- const parsed = new URL(url, "https://placeholder.invalid");
246
- if (SAFE_URL_PROTOCOLS.has(parsed.protocol)) return url;
247
- return "";
248
- } catch {
249
- return "";
250
- }
737
+ return sanitizeUrlWithProtocols(url, SAFE_URL_PROTOCOLS);
738
+ }
739
+ /**
740
+ * Sanitizes a rich-text link destination.
741
+ *
742
+ * In addition to safe web and email URLs, rich-text links may launch the
743
+ * device's phone or messaging app through `tel:` and `sms:`. Relative links
744
+ * remain valid, while executable and resource-only protocols are rejected.
745
+ *
746
+ * @param url - The link destination to sanitize
747
+ * @returns The original URL if safe for an anchor, or an empty string otherwise
748
+ *
749
+ * @example
750
+ * ```ts
751
+ * sanitizeRichTextHref("sms:+15551234567"); // "sms:+15551234567"
752
+ * sanitizeRichTextHref("javascript:alert(1)"); // ""
753
+ * ```
754
+ */ function sanitizeRichTextHref(url) {
755
+ return sanitizeUrlWithProtocols(url, SAFE_RICH_TEXT_HREF_PROTOCOLS);
251
756
  }
252
757
  /**
253
758
  * Normalize a public site URL by stripping any trailing slash from the path.
@@ -392,4 +897,4 @@ var SAFE_URL_PROTOCOLS = new Set([
392
897
  return toAbsoluteSiteUrl(url, siteUrl, sitePathPrefix);
393
898
  }
394
899
  //#endregion
395
- export { toPublicPath as _, getSitePathPrefix as a, __exportAll as b, normalizePath as c, sanitizeUrl as d, slugify as f, toPublicHref as g, toAbsoluteSiteUrl as h, getSiteOrigin as i, normalizeSitePathPrefix as l, toAbsoluteAssetUrl as m, extractDisplayDomain as n, isFullUrl as o, stripSitePathPrefix as p, extractDomain as r, isSafeInternalRedirect as s, buildSiteUrl as t, normalizeSiteUrl as u, toSameSitePath as v, url_exports as y };
900
+ export { toPublicHref as _, getSitePathPrefix as a, url_exports as b, normalizePath as c, sanitizeRichTextHref as d, sanitizeUrl as f, toAbsoluteSiteUrl as g, toAbsoluteAssetUrl as h, getSiteOrigin as i, normalizeSitePathPrefix as l, stripSitePathPrefix as m, extractDisplayDomain as n, isFullUrl as o, slugify as p, extractDomain as r, isSafeInternalRedirect as s, buildSiteUrl as t, normalizeSiteUrl as u, toPublicPath as v, __exportAll as x, toSameSitePath as y };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jant/core",
3
- "version": "0.6.15",
3
+ "version": "0.6.16",
4
4
  "description": "A modern, open-source microblogging platform built on Cloudflare Workers",
5
5
  "type": "module",
6
6
  "exports": {
@@ -0,0 +1,22 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { describe, expect, it } from "vitest";
4
+ import { parse } from "smol-toml";
5
+
6
+ interface MiseConfig {
7
+ tasks?: Record<
8
+ string,
9
+ {
10
+ env?: Record<string, string>;
11
+ }
12
+ >;
13
+ }
14
+
15
+ describe("mise tasks", () => {
16
+ it("enables environment proxy support before the Node dev runtime starts", () => {
17
+ const configPath = resolve(import.meta.dirname, "../../../../mise.toml");
18
+ const config = parse(readFileSync(configPath, "utf8")) as MiseConfig;
19
+
20
+ expect(config.tasks?.["dev-node"]?.env?.NODE_USE_ENV_PROXY).toBe("1");
21
+ });
22
+ });