@kungal/editor-core 0.33.0 → 0.35.0

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,78 @@
1
1
  # @kungal/editor-core
2
2
 
3
+ ## 0.35.0
4
+
5
+ ### Minor Changes
6
+
7
+ - c90e1b9: fix(link): give a typed URL the scheme it needs, in ONE place
8
+
9
+ Typing `www.kungal.com/topic/1` into any link input produced a **relative**
10
+ href: with no scheme the browser resolves it against the current page, so the
11
+ link went to `<origin>/<current/path>/www.kungal.com/topic/1` — dead, and dead
12
+ in a way the author can't see while writing.
13
+
14
+ `insertLinkCommand` now normalizes its `href`, and since every link entry point
15
+ (the selection bubble's input, the headless toolbar's URL panel, the KunUI
16
+ toolbar's popover, and a host's own `linkPrompt` adapter) dispatches that one
17
+ command, all of them are fixed at once — a host must not re-implement this.
18
+
19
+ - `www.kungal.com/topic/1` → `https://www.kungal.com/topic/1` (https, not http:
20
+ an http-only site redirects, an https-only site doesn't). Ports, queries,
21
+ fragments and bare IPv4 hosts included.
22
+ - `me@kungal.com` → `mailto:me@kungal.com` (`https://` would read the address as
23
+ userinfo and go nowhere).
24
+ - Untouched: anything with a scheme — `kungal-user:` / `kungal-reply:` included
25
+ — and anything explicitly relative (`/x`, `./x`, `../x`, `#x`, `?x`, `//host`).
26
+ - The one ambiguous input is a lone dotted word: `readme.md` looks exactly like
27
+ a hostname (`.md` is a TLD), so it gets `https://`. Write `./readme.md` when a
28
+ relative file is what you mean.
29
+
30
+ The rule is also exported as `normalizeLinkHref(input)` from the light,
31
+ zero-dependency `@kungal/editor-core` entry, so a server can normalize legacy
32
+ content with the exact same logic.
33
+
34
+ Along with it, the three URL inputs move from `type="url"` to `type="text"` +
35
+ `inputmode="url"`. Native URL validation rejects exactly the schemeless input
36
+ this change exists to accept — in the KunUI toolbar's popover, which submits a
37
+ real `<form>`, pressing Enter on `www.kungal.com/topic/1` did nothing at all.
38
+ The mobile keyboard hint is kept.
39
+
40
+ ## 0.34.0
41
+
42
+ ### Minor Changes
43
+
44
+ - ab7fbc2: No native `prompt` left: the headless toolbar gets a link URL panel too
45
+
46
+ 0.33.0 gave the selection bubble an inline URL input; the fixed (headless)
47
+ toolbar still popped `window.prompt`. It now opens a small panel under the link
48
+ button — Enter applies, Esc cancels, clicking outside closes, and the editor
49
+ gets focus back. Same behaviour as the bubble otherwise: an existing href is
50
+ prefilled (and selected), and with nothing selected the URL is inserted as
51
+ linked text. A panel rather than an in-place swap because a fixed toolbar row
52
+ that rearranges itself is jarring; the bubble is already a floating layer, so
53
+ there the swap is the right shape.
54
+
55
+ **Breaking (CSS hook, one version old):** `.kun-editor__bubble-input` →
56
+ `.kun-editor__link-input`. Both entry points render the same input, so the hook
57
+ is named after the thing, not the place. Hosts importing
58
+ `@kungal/editor-nuxt/editor.css` need no change; a copied stylesheet wants the
59
+ rename plus the two new hooks:
60
+
61
+ ```css
62
+ .kun-editor__link {
63
+ position: relative;
64
+ } /* structural */
65
+ .kun-editor__link-panel {
66
+ position: absolute;
67
+ } /* structural */
68
+ .kun-editor__link-input {
69
+ /* shared by the toolbar panel and the bubble */
70
+ }
71
+ ```
72
+
73
+ `linkPrompt` keeps taking precedence over all three built-in entries (bubble
74
+ input, toolbar panel, KunUI popover), so a host modal is still one override.
75
+
3
76
  ## 0.33.0
4
77
 
5
78
  ### Minor Changes
@@ -0,0 +1,50 @@
1
+ // src/outline.ts
2
+ var FENCE = /^\s{0,3}(?:```|~~~)/;
3
+ var ATX = /^ {0,3}(#{1,6})\s+(.+?)\s*#*\s*$/;
4
+ var parseHeadings = (markdown) => {
5
+ const out = [];
6
+ let inFence = false;
7
+ for (const line of markdown.split("\n")) {
8
+ if (FENCE.test(line)) {
9
+ inFence = !inFence;
10
+ continue;
11
+ }
12
+ if (inFence) {
13
+ continue;
14
+ }
15
+ const m = line.match(ATX);
16
+ if (m) {
17
+ out.push({ level: m[1].length, text: m[2].trim() });
18
+ }
19
+ }
20
+ return out;
21
+ };
22
+
23
+ // src/href.ts
24
+ var HAS_SCHEME = /^[a-z][a-z0-9+-]*:/i;
25
+ var IS_RELATIVE = /^[/?#.]/;
26
+ var IS_EMAIL = /^[^\s@/]+@[^\s@/]+\.[a-z]{2,}$/i;
27
+ var STARTS_WITH_HOST = /^[^\s/?#@]+\.[a-z]{2,}(?=$|[/?#:])/i;
28
+ var STARTS_WITH_IPV4 = /^\d{1,3}(\.\d{1,3}){3}(?=$|[/?#:])/;
29
+ var normalizeLinkHref = (input) => {
30
+ const href = input.trim();
31
+ if (!href || HAS_SCHEME.test(href) || IS_RELATIVE.test(href)) {
32
+ return href;
33
+ }
34
+ if (IS_EMAIL.test(href)) {
35
+ return `mailto:${href}`;
36
+ }
37
+ if (STARTS_WITH_HOST.test(href) || STARTS_WITH_IPV4.test(href)) {
38
+ return `https://${href}`;
39
+ }
40
+ return href;
41
+ };
42
+
43
+ // src/index.ts
44
+ var MENTION_SCHEME = "kungal-user:";
45
+ var QUOTE_SCHEME = "kungal-reply:";
46
+ var KUN_EDITOR_CORE_VERSION = "0.0.0";
47
+
48
+ export { KUN_EDITOR_CORE_VERSION, MENTION_SCHEME, QUOTE_SCHEME, normalizeLinkHref, parseHeadings };
49
+ //# sourceMappingURL=chunk-IIVFLZTN.js.map
50
+ //# sourceMappingURL=chunk-IIVFLZTN.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/outline.ts","../src/href.ts","../src/index.ts"],"names":[],"mappings":";AAgBA,IAAM,KAAA,GAAQ,qBAAA;AACd,IAAM,GAAA,GAAM,kCAAA;AAGL,IAAM,aAAA,GAAgB,CAAC,QAAA,KAAmC;AAC/D,EAAA,MAAM,MAAoB,EAAC;AAC3B,EAAA,IAAI,OAAA,GAAU,KAAA;AACd,EAAA,KAAA,MAAW,IAAA,IAAQ,QAAA,CAAS,KAAA,CAAM,IAAI,CAAA,EAAG;AACvC,IAAA,IAAI,KAAA,CAAM,IAAA,CAAK,IAAI,CAAA,EAAG;AACpB,MAAA,OAAA,GAAU,CAAC,OAAA;AACX,MAAA;AAAA,IACF;AACA,IAAA,IAAI,OAAA,EAAS;AACX,MAAA;AAAA,IACF;AACA,IAAA,MAAM,CAAA,GAAI,IAAA,CAAK,KAAA,CAAM,GAAG,CAAA;AACxB,IAAA,IAAI,CAAA,EAAG;AACL,MAAA,GAAA,CAAI,IAAA,CAAK,EAAE,KAAA,EAAO,CAAA,CAAE,CAAC,CAAA,CAAG,MAAA,EAAQ,IAAA,EAAM,CAAA,CAAE,CAAC,CAAA,CAAG,IAAA,IAAQ,CAAA;AAAA,IACtD;AAAA,EACF;AACA,EAAA,OAAO,GAAA;AACT;;;ACnBA,IAAM,UAAA,GAAa,qBAAA;AAOnB,IAAM,WAAA,GAAc,SAAA;AAGpB,IAAM,QAAA,GAAW,iCAAA;AAIjB,IAAM,gBAAA,GAAmB,qCAAA;AAGzB,IAAM,gBAAA,GAAmB,oCAAA;AAiBlB,IAAM,iBAAA,GAAoB,CAAC,KAAA,KAA0B;AAC1D,EAAA,MAAM,IAAA,GAAO,MAAM,IAAA,EAAK;AACxB,EAAA,IAAI,CAAC,QAAQ,UAAA,CAAW,IAAA,CAAK,IAAI,CAAA,IAAK,WAAA,CAAY,IAAA,CAAK,IAAI,CAAA,EAAG;AAC5D,IAAA,OAAO,IAAA;AAAA,EACT;AACA,EAAA,IAAI,QAAA,CAAS,IAAA,CAAK,IAAI,CAAA,EAAG;AACvB,IAAA,OAAO,UAAU,IAAI,CAAA,CAAA;AAAA,EACvB;AACA,EAAA,IAAI,iBAAiB,IAAA,CAAK,IAAI,KAAK,gBAAA,CAAiB,IAAA,CAAK,IAAI,CAAA,EAAG;AAC9D,IAAA,OAAO,WAAW,IAAI,CAAA,CAAA;AAAA,EACxB;AACA,EAAA,OAAO,IAAA;AACT;;;AC5CO,IAAM,cAAA,GAAiB;AAMvB,IAAM,YAAA,GAAe;AAErB,IAAM,uBAAA,GAA0B","file":"chunk-IIVFLZTN.js","sourcesContent":["// Heading outline (table-of-contents) parsing — pure, zero-dep, so it lives in\n// the light main entry (a host, or the server, can build a TOC without pulling in\n// @milkdown/kit). Parses ATX headings (`#`…`######`) — what the toolbar produces\n// — and skips fenced code blocks so a `# comment` inside ``` isn't mistaken for a\n// heading. Setext headings (underlined) are intentionally not parsed.\n//\n// The result is an ordered list; a UI renders it and, on click, calls the\n// editor's `scrollToHeading(index)` with the item's array index.\n\nexport interface KunHeading {\n /** Heading level 1–6. */\n level: number\n /** The heading's text (trailing `#` closers stripped). */\n text: string\n}\n\nconst FENCE = /^\\s{0,3}(?:```|~~~)/\nconst ATX = /^ {0,3}(#{1,6})\\s+(.+?)\\s*#*\\s*$/\n\n/** Parse the ordered ATX-heading outline from a markdown string. */\nexport const parseHeadings = (markdown: string): KunHeading[] => {\n const out: KunHeading[] = []\n let inFence = false\n for (const line of markdown.split('\\n')) {\n if (FENCE.test(line)) {\n inFence = !inFence\n continue\n }\n if (inFence) {\n continue\n }\n const m = line.match(ATX)\n if (m) {\n out.push({ level: m[1]!.length, text: m[2]!.trim() })\n }\n }\n return out\n}\n","// Link URL normalization — the ONE place a typed URL becomes a real URL.\n//\n// `www.kungal.com/topic/1` is not a URL: with no scheme the browser resolves it\n// against the current page, so the link silently lands on\n// `<origin>/<current/path>/www.kungal.com/topic/1` — dead, and dead in a way the\n// author cannot see while writing. Every link entry point (the selection\n// bubble's input, the headless toolbar's panel, the KunUI popover, and a host's\n// own `linkPrompt`) dispatches `insertLinkCommand`, so normalizing inside that\n// command covers all of them at once — a host must never re-implement this.\n//\n// It lives in the LIGHT entry (pure string work, zero deps) so a server can\n// import it too, e.g. to normalize legacy content on the way in.\n\n/**\n * A `scheme:` prefix. RFC 3986 allows `.` in a scheme; this deliberately does\n * NOT, so `www.kungal.com:8080/x` reads as host:port instead of as a scheme.\n * (`localhost:3000` still reads as a scheme — genuinely ambiguous, left alone.)\n */\nconst HAS_SCHEME = /^[a-z][a-z0-9+-]*:/i\n\n/**\n * Deliberately relative, so it must survive untouched: `/abs`, `?q`, `#anchor`,\n * `./rel`, `../up`, and protocol-relative `//host` (already valid in a browser).\n * This is also the escape hatch — write `./readme.md` for a relative file.\n */\nconst IS_RELATIVE = /^[/?#.]/\n\n/** A plain email address. `https://` would read it as userinfo and go nowhere. */\nconst IS_EMAIL = /^[^\\s@/]+@[^\\s@/]+\\.[a-z]{2,}$/i\n\n/** Starts with a hostname: a dotted label plus an alphabetic TLD, then end or\n * one of `/ ? # :`. Excludes `@` so a userinfo URL is never invented. */\nconst STARTS_WITH_HOST = /^[^\\s/?#@]+\\.[a-z]{2,}(?=$|[/?#:])/i\n\n/** A bare IPv4 host (`10.0.0.5:8080/x`) — dotted, but it has no TLD to match. */\nconst STARTS_WITH_IPV4 = /^\\d{1,3}(\\.\\d{1,3}){3}(?=$|[/?#:])/\n\n/**\n * Give a user-typed link URL a scheme, so it points where the author meant.\n *\n * - `www.kungal.com/topic/1` → `https://www.kungal.com/topic/1` (https, not\n * http: an http-only site redirects, an https-only site does not).\n * - `me@kungal.com` → `mailto:me@kungal.com`.\n * - Anything already carrying a scheme is returned as typed — including\n * `kungal-user:` / `kungal-reply:` (see MENTION_SCHEME, QUOTE_SCHEME).\n * - Anything explicitly relative (`/x`, `./x`, `#x`, `?x`, `//host`) is left\n * alone, as is anything that doesn't look like a host at all (`draft`).\n *\n * The one ambiguous input is a lone dotted word: `readme.md` looks exactly like\n * a hostname (`.md` IS a TLD), so it gets `https://`. Write `./readme.md` when\n * a relative file is what you mean.\n */\nexport const normalizeLinkHref = (input: string): string => {\n const href = input.trim()\n if (!href || HAS_SCHEME.test(href) || IS_RELATIVE.test(href)) {\n return href\n }\n if (IS_EMAIL.test(href)) {\n return `mailto:${href}`\n }\n if (STARTS_WITH_HOST.test(href) || STARTS_WITH_IPV4.test(href)) {\n return `https://${href}`\n }\n return href\n}\n","// @kungal/editor-core — public entry.\n//\n// STATUS: scaffold. The adapter contracts (the stable public surface) are\n// defined and exported now; the Milkdown plugin ports land incrementally per\n// docs/architecture.md § migration. Consumers should code against the types\n// below — those are the contract that will not churn as plugins move over.\n\nexport * from './types'\n\n// Heading outline (TOC) parsing — pure, so it's here in the light entry.\nexport * from './outline'\n\n// Link URL normalization (`www.a.com/x` → `https://www.a.com/x`). Applied by\n// `insertLinkCommand` for every link entry point; exported here so a server can\n// normalize legacy content with the exact same rules. Pure. See ./href.\nexport * from './href'\n\n// Markdown scheme used to encode an @mention as a plain link the server can\n// render + parse: `[@name](kungal-user:<id>)`. Lives here (not the plugin) so\n// hosts and the server can share the exact string. See ./plugins/mention.\nexport const MENTION_SCHEME = 'kungal-user:'\n\n// Markdown scheme for an inline reference (reply quote): `[label](kungal-reply:<refId>)`.\n// Like MENTION_SCHEME, shared with the server renderer so both agree on the\n// exact string. The reference is opaque here — the host decides what `refId` /\n// `label` mean (see docs/architecture.md § the reply-quote question, option 1).\nexport const QUOTE_SCHEME = 'kungal-reply:'\n\nexport const KUN_EDITOR_CORE_VERSION = '0.0.0'\n\n// ── The Milkdown plugins live in the `./preset` subpath ──────────────────────\n// This main entry stays light on purpose: types + MENTION_SCHEME, ZERO runtime\n// deps, so the server (which only needs the @mention scheme string) can import\n// it without installing @milkdown/kit / katex / codemirror.\n//\n// The composed Milkdown bundle and the individual plugin factories are exported\n// from `@kungal/editor-core/preset` (they pull in the peer deps):\n//\n// import { createKunEditorPlugins } from '@kungal/editor-core/preset'\n//\n// P1 landed (docs/architecture.md § migration): spoiler, katex, code-block,\n// stop-link — each a factory (createXxxPlugin), never a host-bound singleton.\n// P2 adds the adapter-driven plugins (upload / mention / sticker).\n"]}
package/dist/index.cjs CHANGED
@@ -22,6 +22,26 @@ var parseHeadings = (markdown) => {
22
22
  return out;
23
23
  };
24
24
 
25
+ // src/href.ts
26
+ var HAS_SCHEME = /^[a-z][a-z0-9+-]*:/i;
27
+ var IS_RELATIVE = /^[/?#.]/;
28
+ var IS_EMAIL = /^[^\s@/]+@[^\s@/]+\.[a-z]{2,}$/i;
29
+ var STARTS_WITH_HOST = /^[^\s/?#@]+\.[a-z]{2,}(?=$|[/?#:])/i;
30
+ var STARTS_WITH_IPV4 = /^\d{1,3}(\.\d{1,3}){3}(?=$|[/?#:])/;
31
+ var normalizeLinkHref = (input) => {
32
+ const href = input.trim();
33
+ if (!href || HAS_SCHEME.test(href) || IS_RELATIVE.test(href)) {
34
+ return href;
35
+ }
36
+ if (IS_EMAIL.test(href)) {
37
+ return `mailto:${href}`;
38
+ }
39
+ if (STARTS_WITH_HOST.test(href) || STARTS_WITH_IPV4.test(href)) {
40
+ return `https://${href}`;
41
+ }
42
+ return href;
43
+ };
44
+
25
45
  // src/index.ts
26
46
  var MENTION_SCHEME = "kungal-user:";
27
47
  var QUOTE_SCHEME = "kungal-reply:";
@@ -30,6 +50,7 @@ var KUN_EDITOR_CORE_VERSION = "0.0.0";
30
50
  exports.KUN_EDITOR_CORE_VERSION = KUN_EDITOR_CORE_VERSION;
31
51
  exports.MENTION_SCHEME = MENTION_SCHEME;
32
52
  exports.QUOTE_SCHEME = QUOTE_SCHEME;
53
+ exports.normalizeLinkHref = normalizeLinkHref;
33
54
  exports.parseHeadings = parseHeadings;
34
55
  //# sourceMappingURL=index.cjs.map
35
56
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/outline.ts","../src/index.ts"],"names":[],"mappings":";;;AAgBA,IAAM,KAAA,GAAQ,qBAAA;AACd,IAAM,GAAA,GAAM,kCAAA;AAGL,IAAM,aAAA,GAAgB,CAAC,QAAA,KAAmC;AAC/D,EAAA,MAAM,MAAoB,EAAC;AAC3B,EAAA,IAAI,OAAA,GAAU,KAAA;AACd,EAAA,KAAA,MAAW,IAAA,IAAQ,QAAA,CAAS,KAAA,CAAM,IAAI,CAAA,EAAG;AACvC,IAAA,IAAI,KAAA,CAAM,IAAA,CAAK,IAAI,CAAA,EAAG;AACpB,MAAA,OAAA,GAAU,CAAC,OAAA;AACX,MAAA;AAAA,IACF;AACA,IAAA,IAAI,OAAA,EAAS;AACX,MAAA;AAAA,IACF;AACA,IAAA,MAAM,CAAA,GAAI,IAAA,CAAK,KAAA,CAAM,GAAG,CAAA;AACxB,IAAA,IAAI,CAAA,EAAG;AACL,MAAA,GAAA,CAAI,IAAA,CAAK,EAAE,KAAA,EAAO,CAAA,CAAE,CAAC,CAAA,CAAG,MAAA,EAAQ,IAAA,EAAM,CAAA,CAAE,CAAC,CAAA,CAAG,IAAA,IAAQ,CAAA;AAAA,IACtD;AAAA,EACF;AACA,EAAA,OAAO,GAAA;AACT;;;ACtBO,IAAM,cAAA,GAAiB;AAMvB,IAAM,YAAA,GAAe;AAErB,IAAM,uBAAA,GAA0B","file":"index.cjs","sourcesContent":["// Heading outline (table-of-contents) parsing — pure, zero-dep, so it lives in\n// the light main entry (a host, or the server, can build a TOC without pulling in\n// @milkdown/kit). Parses ATX headings (`#`…`######`) — what the toolbar produces\n// — and skips fenced code blocks so a `# comment` inside ``` isn't mistaken for a\n// heading. Setext headings (underlined) are intentionally not parsed.\n//\n// The result is an ordered list; a UI renders it and, on click, calls the\n// editor's `scrollToHeading(index)` with the item's array index.\n\nexport interface KunHeading {\n /** Heading level 1–6. */\n level: number\n /** The heading's text (trailing `#` closers stripped). */\n text: string\n}\n\nconst FENCE = /^\\s{0,3}(?:```|~~~)/\nconst ATX = /^ {0,3}(#{1,6})\\s+(.+?)\\s*#*\\s*$/\n\n/** Parse the ordered ATX-heading outline from a markdown string. */\nexport const parseHeadings = (markdown: string): KunHeading[] => {\n const out: KunHeading[] = []\n let inFence = false\n for (const line of markdown.split('\\n')) {\n if (FENCE.test(line)) {\n inFence = !inFence\n continue\n }\n if (inFence) {\n continue\n }\n const m = line.match(ATX)\n if (m) {\n out.push({ level: m[1]!.length, text: m[2]!.trim() })\n }\n }\n return out\n}\n","// @kungal/editor-core — public entry.\n//\n// STATUS: scaffold. The adapter contracts (the stable public surface) are\n// defined and exported now; the Milkdown plugin ports land incrementally per\n// docs/architecture.md § migration. Consumers should code against the types\n// below — those are the contract that will not churn as plugins move over.\n\nexport * from './types'\n\n// Heading outline (TOC) parsing — pure, so it's here in the light entry.\nexport * from './outline'\n\n// Markdown scheme used to encode an @mention as a plain link the server can\n// render + parse: `[@name](kungal-user:<id>)`. Lives here (not the plugin) so\n// hosts and the server can share the exact string. See ./plugins/mention.\nexport const MENTION_SCHEME = 'kungal-user:'\n\n// Markdown scheme for an inline reference (reply quote): `[label](kungal-reply:<refId>)`.\n// Like MENTION_SCHEME, shared with the server renderer so both agree on the\n// exact string. The reference is opaque here — the host decides what `refId` /\n// `label` mean (see docs/architecture.md § the reply-quote question, option 1).\nexport const QUOTE_SCHEME = 'kungal-reply:'\n\nexport const KUN_EDITOR_CORE_VERSION = '0.0.0'\n\n// ── The Milkdown plugins live in the `./preset` subpath ──────────────────────\n// This main entry stays light on purpose: types + MENTION_SCHEME, ZERO runtime\n// deps, so the server (which only needs the @mention scheme string) can import\n// it without installing @milkdown/kit / katex / codemirror.\n//\n// The composed Milkdown bundle and the individual plugin factories are exported\n// from `@kungal/editor-core/preset` (they pull in the peer deps):\n//\n// import { createKunEditorPlugins } from '@kungal/editor-core/preset'\n//\n// P1 landed (docs/architecture.md § migration): spoiler, katex, code-block,\n// stop-link — each a factory (createXxxPlugin), never a host-bound singleton.\n// P2 adds the adapter-driven plugins (upload / mention / sticker).\n"]}
1
+ {"version":3,"sources":["../src/outline.ts","../src/href.ts","../src/index.ts"],"names":[],"mappings":";;;AAgBA,IAAM,KAAA,GAAQ,qBAAA;AACd,IAAM,GAAA,GAAM,kCAAA;AAGL,IAAM,aAAA,GAAgB,CAAC,QAAA,KAAmC;AAC/D,EAAA,MAAM,MAAoB,EAAC;AAC3B,EAAA,IAAI,OAAA,GAAU,KAAA;AACd,EAAA,KAAA,MAAW,IAAA,IAAQ,QAAA,CAAS,KAAA,CAAM,IAAI,CAAA,EAAG;AACvC,IAAA,IAAI,KAAA,CAAM,IAAA,CAAK,IAAI,CAAA,EAAG;AACpB,MAAA,OAAA,GAAU,CAAC,OAAA;AACX,MAAA;AAAA,IACF;AACA,IAAA,IAAI,OAAA,EAAS;AACX,MAAA;AAAA,IACF;AACA,IAAA,MAAM,CAAA,GAAI,IAAA,CAAK,KAAA,CAAM,GAAG,CAAA;AACxB,IAAA,IAAI,CAAA,EAAG;AACL,MAAA,GAAA,CAAI,IAAA,CAAK,EAAE,KAAA,EAAO,CAAA,CAAE,CAAC,CAAA,CAAG,MAAA,EAAQ,IAAA,EAAM,CAAA,CAAE,CAAC,CAAA,CAAG,IAAA,IAAQ,CAAA;AAAA,IACtD;AAAA,EACF;AACA,EAAA,OAAO,GAAA;AACT;;;ACnBA,IAAM,UAAA,GAAa,qBAAA;AAOnB,IAAM,WAAA,GAAc,SAAA;AAGpB,IAAM,QAAA,GAAW,iCAAA;AAIjB,IAAM,gBAAA,GAAmB,qCAAA;AAGzB,IAAM,gBAAA,GAAmB,oCAAA;AAiBlB,IAAM,iBAAA,GAAoB,CAAC,KAAA,KAA0B;AAC1D,EAAA,MAAM,IAAA,GAAO,MAAM,IAAA,EAAK;AACxB,EAAA,IAAI,CAAC,QAAQ,UAAA,CAAW,IAAA,CAAK,IAAI,CAAA,IAAK,WAAA,CAAY,IAAA,CAAK,IAAI,CAAA,EAAG;AAC5D,IAAA,OAAO,IAAA;AAAA,EACT;AACA,EAAA,IAAI,QAAA,CAAS,IAAA,CAAK,IAAI,CAAA,EAAG;AACvB,IAAA,OAAO,UAAU,IAAI,CAAA,CAAA;AAAA,EACvB;AACA,EAAA,IAAI,iBAAiB,IAAA,CAAK,IAAI,KAAK,gBAAA,CAAiB,IAAA,CAAK,IAAI,CAAA,EAAG;AAC9D,IAAA,OAAO,WAAW,IAAI,CAAA,CAAA;AAAA,EACxB;AACA,EAAA,OAAO,IAAA;AACT;;;AC5CO,IAAM,cAAA,GAAiB;AAMvB,IAAM,YAAA,GAAe;AAErB,IAAM,uBAAA,GAA0B","file":"index.cjs","sourcesContent":["// Heading outline (table-of-contents) parsing — pure, zero-dep, so it lives in\n// the light main entry (a host, or the server, can build a TOC without pulling in\n// @milkdown/kit). Parses ATX headings (`#`…`######`) — what the toolbar produces\n// — and skips fenced code blocks so a `# comment` inside ``` isn't mistaken for a\n// heading. Setext headings (underlined) are intentionally not parsed.\n//\n// The result is an ordered list; a UI renders it and, on click, calls the\n// editor's `scrollToHeading(index)` with the item's array index.\n\nexport interface KunHeading {\n /** Heading level 1–6. */\n level: number\n /** The heading's text (trailing `#` closers stripped). */\n text: string\n}\n\nconst FENCE = /^\\s{0,3}(?:```|~~~)/\nconst ATX = /^ {0,3}(#{1,6})\\s+(.+?)\\s*#*\\s*$/\n\n/** Parse the ordered ATX-heading outline from a markdown string. */\nexport const parseHeadings = (markdown: string): KunHeading[] => {\n const out: KunHeading[] = []\n let inFence = false\n for (const line of markdown.split('\\n')) {\n if (FENCE.test(line)) {\n inFence = !inFence\n continue\n }\n if (inFence) {\n continue\n }\n const m = line.match(ATX)\n if (m) {\n out.push({ level: m[1]!.length, text: m[2]!.trim() })\n }\n }\n return out\n}\n","// Link URL normalization — the ONE place a typed URL becomes a real URL.\n//\n// `www.kungal.com/topic/1` is not a URL: with no scheme the browser resolves it\n// against the current page, so the link silently lands on\n// `<origin>/<current/path>/www.kungal.com/topic/1` — dead, and dead in a way the\n// author cannot see while writing. Every link entry point (the selection\n// bubble's input, the headless toolbar's panel, the KunUI popover, and a host's\n// own `linkPrompt`) dispatches `insertLinkCommand`, so normalizing inside that\n// command covers all of them at once — a host must never re-implement this.\n//\n// It lives in the LIGHT entry (pure string work, zero deps) so a server can\n// import it too, e.g. to normalize legacy content on the way in.\n\n/**\n * A `scheme:` prefix. RFC 3986 allows `.` in a scheme; this deliberately does\n * NOT, so `www.kungal.com:8080/x` reads as host:port instead of as a scheme.\n * (`localhost:3000` still reads as a scheme — genuinely ambiguous, left alone.)\n */\nconst HAS_SCHEME = /^[a-z][a-z0-9+-]*:/i\n\n/**\n * Deliberately relative, so it must survive untouched: `/abs`, `?q`, `#anchor`,\n * `./rel`, `../up`, and protocol-relative `//host` (already valid in a browser).\n * This is also the escape hatch — write `./readme.md` for a relative file.\n */\nconst IS_RELATIVE = /^[/?#.]/\n\n/** A plain email address. `https://` would read it as userinfo and go nowhere. */\nconst IS_EMAIL = /^[^\\s@/]+@[^\\s@/]+\\.[a-z]{2,}$/i\n\n/** Starts with a hostname: a dotted label plus an alphabetic TLD, then end or\n * one of `/ ? # :`. Excludes `@` so a userinfo URL is never invented. */\nconst STARTS_WITH_HOST = /^[^\\s/?#@]+\\.[a-z]{2,}(?=$|[/?#:])/i\n\n/** A bare IPv4 host (`10.0.0.5:8080/x`) — dotted, but it has no TLD to match. */\nconst STARTS_WITH_IPV4 = /^\\d{1,3}(\\.\\d{1,3}){3}(?=$|[/?#:])/\n\n/**\n * Give a user-typed link URL a scheme, so it points where the author meant.\n *\n * - `www.kungal.com/topic/1` → `https://www.kungal.com/topic/1` (https, not\n * http: an http-only site redirects, an https-only site does not).\n * - `me@kungal.com` → `mailto:me@kungal.com`.\n * - Anything already carrying a scheme is returned as typed — including\n * `kungal-user:` / `kungal-reply:` (see MENTION_SCHEME, QUOTE_SCHEME).\n * - Anything explicitly relative (`/x`, `./x`, `#x`, `?x`, `//host`) is left\n * alone, as is anything that doesn't look like a host at all (`draft`).\n *\n * The one ambiguous input is a lone dotted word: `readme.md` looks exactly like\n * a hostname (`.md` IS a TLD), so it gets `https://`. Write `./readme.md` when\n * a relative file is what you mean.\n */\nexport const normalizeLinkHref = (input: string): string => {\n const href = input.trim()\n if (!href || HAS_SCHEME.test(href) || IS_RELATIVE.test(href)) {\n return href\n }\n if (IS_EMAIL.test(href)) {\n return `mailto:${href}`\n }\n if (STARTS_WITH_HOST.test(href) || STARTS_WITH_IPV4.test(href)) {\n return `https://${href}`\n }\n return href\n}\n","// @kungal/editor-core — public entry.\n//\n// STATUS: scaffold. The adapter contracts (the stable public surface) are\n// defined and exported now; the Milkdown plugin ports land incrementally per\n// docs/architecture.md § migration. Consumers should code against the types\n// below — those are the contract that will not churn as plugins move over.\n\nexport * from './types'\n\n// Heading outline (TOC) parsing — pure, so it's here in the light entry.\nexport * from './outline'\n\n// Link URL normalization (`www.a.com/x` → `https://www.a.com/x`). Applied by\n// `insertLinkCommand` for every link entry point; exported here so a server can\n// normalize legacy content with the exact same rules. Pure. See ./href.\nexport * from './href'\n\n// Markdown scheme used to encode an @mention as a plain link the server can\n// render + parse: `[@name](kungal-user:<id>)`. Lives here (not the plugin) so\n// hosts and the server can share the exact string. See ./plugins/mention.\nexport const MENTION_SCHEME = 'kungal-user:'\n\n// Markdown scheme for an inline reference (reply quote): `[label](kungal-reply:<refId>)`.\n// Like MENTION_SCHEME, shared with the server renderer so both agree on the\n// exact string. The reference is opaque here — the host decides what `refId` /\n// `label` mean (see docs/architecture.md § the reply-quote question, option 1).\nexport const QUOTE_SCHEME = 'kungal-reply:'\n\nexport const KUN_EDITOR_CORE_VERSION = '0.0.0'\n\n// ── The Milkdown plugins live in the `./preset` subpath ──────────────────────\n// This main entry stays light on purpose: types + MENTION_SCHEME, ZERO runtime\n// deps, so the server (which only needs the @mention scheme string) can import\n// it without installing @milkdown/kit / katex / codemirror.\n//\n// The composed Milkdown bundle and the individual plugin factories are exported\n// from `@kungal/editor-core/preset` (they pull in the peer deps):\n//\n// import { createKunEditorPlugins } from '@kungal/editor-core/preset'\n//\n// P1 landed (docs/architecture.md § migration): spoiler, katex, code-block,\n// stop-link — each a factory (createXxxPlugin), never a host-bound singleton.\n// P2 adds the adapter-driven plugins (upload / mention / sticker).\n"]}
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- export { K as KunEditorAdapters, a as KunEditorFeatures, b as KunEditorLocale, L as LinkPrompt, M as MentionUser, N as Notify, c as NotifyLevel, S as SearchMentionUsers, d as StickerItem, e as StickerPack, f as StickerSource, U as UploadImage } from './types-DqQwYYJ1.cjs';
1
+ export { K as KunEditorAdapters, a as KunEditorFeatures, b as KunEditorLocale, L as LinkPrompt, M as MentionUser, N as Notify, c as NotifyLevel, S as SearchMentionUsers, d as StickerItem, e as StickerPack, f as StickerSource, U as UploadImage } from './types-f8Szrgvw.cjs';
2
2
 
3
3
  interface KunHeading {
4
4
  /** Heading level 1–6. */
@@ -9,8 +9,25 @@ interface KunHeading {
9
9
  /** Parse the ordered ATX-heading outline from a markdown string. */
10
10
  declare const parseHeadings: (markdown: string) => KunHeading[];
11
11
 
12
+ /**
13
+ * Give a user-typed link URL a scheme, so it points where the author meant.
14
+ *
15
+ * - `www.kungal.com/topic/1` → `https://www.kungal.com/topic/1` (https, not
16
+ * http: an http-only site redirects, an https-only site does not).
17
+ * - `me@kungal.com` → `mailto:me@kungal.com`.
18
+ * - Anything already carrying a scheme is returned as typed — including
19
+ * `kungal-user:` / `kungal-reply:` (see MENTION_SCHEME, QUOTE_SCHEME).
20
+ * - Anything explicitly relative (`/x`, `./x`, `#x`, `?x`, `//host`) is left
21
+ * alone, as is anything that doesn't look like a host at all (`draft`).
22
+ *
23
+ * The one ambiguous input is a lone dotted word: `readme.md` looks exactly like
24
+ * a hostname (`.md` IS a TLD), so it gets `https://`. Write `./readme.md` when
25
+ * a relative file is what you mean.
26
+ */
27
+ declare const normalizeLinkHref: (input: string) => string;
28
+
12
29
  declare const MENTION_SCHEME = "kungal-user:";
13
30
  declare const QUOTE_SCHEME = "kungal-reply:";
14
31
  declare const KUN_EDITOR_CORE_VERSION = "0.0.0";
15
32
 
16
- export { KUN_EDITOR_CORE_VERSION, type KunHeading, MENTION_SCHEME, QUOTE_SCHEME, parseHeadings };
33
+ export { KUN_EDITOR_CORE_VERSION, type KunHeading, MENTION_SCHEME, QUOTE_SCHEME, normalizeLinkHref, parseHeadings };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { K as KunEditorAdapters, a as KunEditorFeatures, b as KunEditorLocale, L as LinkPrompt, M as MentionUser, N as Notify, c as NotifyLevel, S as SearchMentionUsers, d as StickerItem, e as StickerPack, f as StickerSource, U as UploadImage } from './types-DqQwYYJ1.js';
1
+ export { K as KunEditorAdapters, a as KunEditorFeatures, b as KunEditorLocale, L as LinkPrompt, M as MentionUser, N as Notify, c as NotifyLevel, S as SearchMentionUsers, d as StickerItem, e as StickerPack, f as StickerSource, U as UploadImage } from './types-f8Szrgvw.js';
2
2
 
3
3
  interface KunHeading {
4
4
  /** Heading level 1–6. */
@@ -9,8 +9,25 @@ interface KunHeading {
9
9
  /** Parse the ordered ATX-heading outline from a markdown string. */
10
10
  declare const parseHeadings: (markdown: string) => KunHeading[];
11
11
 
12
+ /**
13
+ * Give a user-typed link URL a scheme, so it points where the author meant.
14
+ *
15
+ * - `www.kungal.com/topic/1` → `https://www.kungal.com/topic/1` (https, not
16
+ * http: an http-only site redirects, an https-only site does not).
17
+ * - `me@kungal.com` → `mailto:me@kungal.com`.
18
+ * - Anything already carrying a scheme is returned as typed — including
19
+ * `kungal-user:` / `kungal-reply:` (see MENTION_SCHEME, QUOTE_SCHEME).
20
+ * - Anything explicitly relative (`/x`, `./x`, `#x`, `?x`, `//host`) is left
21
+ * alone, as is anything that doesn't look like a host at all (`draft`).
22
+ *
23
+ * The one ambiguous input is a lone dotted word: `readme.md` looks exactly like
24
+ * a hostname (`.md` IS a TLD), so it gets `https://`. Write `./readme.md` when
25
+ * a relative file is what you mean.
26
+ */
27
+ declare const normalizeLinkHref: (input: string) => string;
28
+
12
29
  declare const MENTION_SCHEME = "kungal-user:";
13
30
  declare const QUOTE_SCHEME = "kungal-reply:";
14
31
  declare const KUN_EDITOR_CORE_VERSION = "0.0.0";
15
32
 
16
- export { KUN_EDITOR_CORE_VERSION, type KunHeading, MENTION_SCHEME, QUOTE_SCHEME, parseHeadings };
33
+ export { KUN_EDITOR_CORE_VERSION, type KunHeading, MENTION_SCHEME, QUOTE_SCHEME, normalizeLinkHref, parseHeadings };
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
- export { KUN_EDITOR_CORE_VERSION, MENTION_SCHEME, QUOTE_SCHEME, parseHeadings } from './chunk-PT42ZQGO.js';
1
+ export { KUN_EDITOR_CORE_VERSION, MENTION_SCHEME, QUOTE_SCHEME, normalizeLinkHref, parseHeadings } from './chunk-IIVFLZTN.js';
2
2
  //# sourceMappingURL=index.js.map
3
3
  //# sourceMappingURL=index.js.map
@@ -218,10 +218,32 @@ var clearOrphanLink = utils.$prose(
218
218
  })
219
219
  );
220
220
  var createStopLinkPlugin = () => [stopLinkCommand, linkCustomKeymap, clearOrphanLink].flat();
221
+
222
+ // src/href.ts
223
+ var HAS_SCHEME = /^[a-z][a-z0-9+-]*:/i;
224
+ var IS_RELATIVE = /^[/?#.]/;
225
+ var IS_EMAIL = /^[^\s@/]+@[^\s@/]+\.[a-z]{2,}$/i;
226
+ var STARTS_WITH_HOST = /^[^\s/?#@]+\.[a-z]{2,}(?=$|[/?#:])/i;
227
+ var STARTS_WITH_IPV4 = /^\d{1,3}(\.\d{1,3}){3}(?=$|[/?#:])/;
228
+ var normalizeLinkHref = (input) => {
229
+ const href = input.trim();
230
+ if (!href || HAS_SCHEME.test(href) || IS_RELATIVE.test(href)) {
231
+ return href;
232
+ }
233
+ if (IS_EMAIL.test(href)) {
234
+ return `mailto:${href}`;
235
+ }
236
+ if (STARTS_WITH_HOST.test(href) || STARTS_WITH_IPV4.test(href)) {
237
+ return `https://${href}`;
238
+ }
239
+ return href;
240
+ };
241
+
242
+ // src/plugins/link/index.ts
221
243
  var insertLinkCommand = utils.$command(
222
244
  "InsertKunLink",
223
245
  (ctx) => (payload) => (state, dispatch) => {
224
- const href = payload?.href?.trim();
246
+ const href = normalizeLinkHref(payload?.href ?? "");
225
247
  if (!href) {
226
248
  return false;
227
249
  }