@bendyline/squisq-editor-react 2.8.0 → 2.10.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.
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  markdownToTiptap,
3
3
  tiptapToMarkdown
4
- } from "./chunk-K2WJYVS4.js";
4
+ } from "./chunk-TCLNRATG.js";
5
5
 
6
6
  // src/jsonEditor/JsonEditorContext.tsx
7
7
  import { createContext, useContext, useEffect, useMemo } from "react";
@@ -0,0 +1,144 @@
1
+ // src/proofing/harperProvider.ts
2
+ import {
3
+ categorizeLintKind
4
+ } from "@bendyline/squisq/proof";
5
+ var SUGGESTION_KINDS = ["replace", "remove", "insertAfter"];
6
+ var MAX_IGNORE_ROUNDS = 5;
7
+ function createHarperProofingProvider(config = {}) {
8
+ let enginePromise = null;
9
+ let lastPass = null;
10
+ let disposed = false;
11
+ const words = new Set(config.initialWords ?? []);
12
+ const load = async () => {
13
+ const harper = await import("harper.js");
14
+ let binary = config.binary;
15
+ if (binary === void 0) {
16
+ if (config.wasmUrl) {
17
+ const absolute = typeof location !== "undefined" ? new URL(config.wasmUrl, location.href).toString() : config.wasmUrl;
18
+ binary = harper.createBinaryModuleFromUrl(absolute);
19
+ } else {
20
+ binary = (await import("harper.js/binary")).binary;
21
+ }
22
+ }
23
+ const dialect = harper.Dialect[config.dialect ?? "American"];
24
+ const init = { binary, dialect };
25
+ const linter = typeof Worker !== "undefined" ? new harper.WorkerLinter(init) : new harper.LocalLinter(init);
26
+ await linter.setup();
27
+ if (words.size > 0) await linter.importWords([...words]);
28
+ return { harper, linter };
29
+ };
30
+ const ensure = () => {
31
+ if (disposed) return Promise.reject(new Error("ProofingProvider has been disposed"));
32
+ if (!enginePromise) {
33
+ enginePromise = load().catch((err) => {
34
+ enginePromise = null;
35
+ throw err;
36
+ });
37
+ }
38
+ return enginePromise;
39
+ };
40
+ const toFinding = (lint, id, text) => {
41
+ const span = lint.span();
42
+ const kind = lint.lint_kind();
43
+ return {
44
+ id,
45
+ start: span.start,
46
+ end: span.end,
47
+ category: categorizeLintKind(kind),
48
+ kind,
49
+ message: lint.message(),
50
+ originalText: text.slice(span.start, span.end),
51
+ suggestions: lint.suggestions().map((suggestion) => ({
52
+ text: suggestion.get_replacement_text(),
53
+ kind: SUGGESTION_KINDS[suggestion.kind()] ?? "replace"
54
+ }))
55
+ };
56
+ };
57
+ let passCounter = 0;
58
+ return {
59
+ // App-level additions only persist if the host gave us somewhere to
60
+ // put them; without a callback the editor hides that menu item.
61
+ hasAppDictionary: config.onDictionaryWord != null,
62
+ async setup() {
63
+ await ensure();
64
+ },
65
+ async lint(text, options) {
66
+ const { linter } = await ensure();
67
+ const language = options?.language ?? "plaintext";
68
+ const lints = await linter.lint(text, { language });
69
+ passCounter += 1;
70
+ const byId = /* @__PURE__ */ new Map();
71
+ const findings = lints.map((lint, index) => {
72
+ const id = `${passCounter}:${index}`;
73
+ const finding = toFinding(lint, id, text);
74
+ byId.set(id, { lint, start: finding.start, end: finding.end });
75
+ return finding;
76
+ });
77
+ lastPass = { source: text, language, byId };
78
+ return findings;
79
+ },
80
+ async ignoreFinding(findingId) {
81
+ const { linter } = await ensure();
82
+ const pass = lastPass;
83
+ const entry = pass?.byId.get(findingId);
84
+ if (!pass || !entry) {
85
+ console.warn("squisq proofing: ignoreFinding called with a stale finding id", findingId);
86
+ return;
87
+ }
88
+ let lint = entry.lint;
89
+ for (let round = 0; round < MAX_IGNORE_ROUNDS && lint; round += 1) {
90
+ await linter.ignoreLint(pass.source, lint);
91
+ const relint = await linter.lint(pass.source, { language: pass.language });
92
+ lint = relint.find(
93
+ (candidate) => candidate.span().start === entry.start && candidate.span().end === entry.end
94
+ );
95
+ }
96
+ },
97
+ async addWord(word) {
98
+ const { linter } = await ensure();
99
+ words.add(word);
100
+ await linter.importWords([...words]);
101
+ config.onDictionaryWord?.(word);
102
+ },
103
+ async addWords(list) {
104
+ if (list.length === 0) return;
105
+ const { linter } = await ensure();
106
+ for (const word of list) words.add(word);
107
+ await linter.importWords([...words]);
108
+ },
109
+ async exportIgnored() {
110
+ const { linter } = await ensure();
111
+ return linter.exportIgnoredLints();
112
+ },
113
+ async clearIgnored() {
114
+ const { linter } = await ensure();
115
+ await linter.clearIgnoredLints();
116
+ },
117
+ async importIgnored(json) {
118
+ const { linter } = await ensure();
119
+ await linter.importIgnoredLints(json);
120
+ },
121
+ async setDialect(dialect) {
122
+ const { harper, linter } = await ensure();
123
+ await linter.setDialect(harper.Dialect[dialect]);
124
+ },
125
+ dispose() {
126
+ if (disposed) return;
127
+ disposed = true;
128
+ lastPass = null;
129
+ const pending = enginePromise;
130
+ enginePromise = null;
131
+ void pending?.then((handle) => handle.linter.dispose()).catch(() => {
132
+ });
133
+ }
134
+ };
135
+ }
136
+
137
+ // src/proofing/index.ts
138
+ import { PROOF_DIALECTS, PROOF_FRONTMATTER_KEYS } from "@bendyline/squisq/proof";
139
+
140
+ export {
141
+ createHarperProofingProvider,
142
+ PROOF_DIALECTS,
143
+ PROOF_FRONTMATTER_KEYS
144
+ };
@@ -10,6 +10,8 @@ var RE_BOLD_UNDER = /(?<![\p{L}\p{N}_])__(?![\s_])(.+?)(?<![\s_])__(?![\p{L}\p{N
10
10
  var RE_ITALIC_STAR = /\*(?![\s*])(.+?)(?<![\s*])\*/g;
11
11
  var RE_ITALIC_UNDER = /(?<![\p{L}\p{N}_])_(?![\s_])(.+?)(?<![\s_])_(?![\p{L}\p{N}_])/gu;
12
12
  var RE_STRIKETHROUGH = /~~(.+?)~~/g;
13
+ var RE_LEGACY_BOLD_STAR = /\*\*(?![\s*])(.+?)(?<![\s*])([ \t]+)\*\*(?=$|[^\s*])/g;
14
+ var RE_LEGACY_ITALIC_STAR = /(?<!\*)\*(?![\s*])(.+?)(?<![\s*])([ \t]+)\*(?=$|[^\s*])/g;
13
15
  var ESCAPABLE_MD_CHARS = ["*", "_", "~", "\\"];
14
16
  var RE_MD_ESCAPE = /\\([*_~\\])/g;
15
17
  var RE_INLINE_CODE = /`(.+?)`/g;
@@ -18,13 +20,9 @@ var RE_MENTION = /@\[([^\]]+?)\]\(([a-z][a-z0-9+.-]*)\\?:([^)\s]+)\)/gi;
18
20
  var RE_MENTION_TAG = /<span\b[^>]*?\bdata-mention\b[^>]*?>(?:<[^>]+>)*([^<]*)<\/span>/gi;
19
21
  var RE_ICON_MD = /\{\[([a-zA-Z0-9_:-]+)\]\}/g;
20
22
  var RE_ICON_TAG = /<i\b[^>]*?\bdata-icon="([^"]*)"[^>]*?><\/i>/gi;
21
- var RE_STRONG_TAG = /<strong>(.*?)<\/strong>/g;
22
- var RE_B_TAG = /<b>(.*?)<\/b>/g;
23
- var RE_EM_TAG = /<em>(.*?)<\/em>/g;
24
- var RE_I_TAG = /<i>(.*?)<\/i>/g;
25
- var RE_S_TAG = /<s>(.*?)<\/s>/g;
26
- var RE_DEL_TAG = /<del>(.*?)<\/del>/g;
23
+ var RE_MARK_TAG = /<(strong|b|em|i|s|del|sup|sub)>(.*?)<\/\1>/gs;
27
24
  var RE_CODE_TAG = /<code>(.*?)<\/code>/g;
25
+ var RE_VERT_ALIGN_TAG = /<(sup|sub)>((?:(?!<\/?(?:sup|sub)>)[\s\S])*)<\/\1>/gi;
28
26
  var RE_A_TAG = /<a\b([^>]*)>(.*?)<\/a>/g;
29
27
  var RE_IMG_TAG = /<img\b([^>]*)>/g;
30
28
  var RE_STRIP_TAGS = /<[^>]+>/g;
@@ -787,6 +785,10 @@ function inlineToHtml(text) {
787
785
  RE_INLINE_CODE,
788
786
  (_m, code) => stash(`<code>${escapeHtml(code)}</code>`)
789
787
  );
788
+ staged = staged.replace(RE_VERT_ALIGN_TAG, (_m, tag, inner) => {
789
+ const name = tag.toLowerCase();
790
+ return stash(`<${name}>${inlineToHtml(inner)}</${name}>`);
791
+ });
790
792
  staged = staged.replace(
791
793
  RE_IMAGE,
792
794
  (_m, alt, src) => stash(`<img alt="${escapeHtml(alt)}" src="${escapeHtml(src)}">`)
@@ -814,8 +816,10 @@ function inlineToHtml(text) {
814
816
  RE_MD_ESCAPE,
815
817
  (_m, ch) => `\0ESC${ESCAPABLE_MD_CHARS.indexOf(ch)}\0`
816
818
  );
819
+ result = result.replace(RE_LEGACY_BOLD_STAR, "<strong>$1</strong>$2");
817
820
  result = result.replace(RE_BOLD_STAR, "<strong>$1</strong>");
818
821
  result = result.replace(RE_BOLD_UNDER, "<strong>$1</strong>");
822
+ result = result.replace(RE_LEGACY_ITALIC_STAR, "<em>$1</em>$2");
819
823
  result = result.replace(RE_ITALIC_STAR, "<em>$1</em>");
820
824
  result = result.replace(RE_ITALIC_UNDER, "<em>$1</em>");
821
825
  result = result.replace(RE_STRIKETHROUGH, "<s>$1</s>");
@@ -961,17 +965,46 @@ function escapeMarkdownTextNodes(html) {
961
965
  }
962
966
  return out + escapeMarkdownText(html.slice(cursor));
963
967
  }
968
+ function markdownFromMarkTags(html) {
969
+ return html.replace(RE_MARK_TAG, (_match, tag, inner) => {
970
+ const convertedInner = markdownFromMarkTags(inner);
971
+ const leading = convertedInner.match(/^\s*/)?.[0] ?? "";
972
+ const trailing = convertedInner.match(/\s*$/)?.[0] ?? "";
973
+ const content = convertedInner.slice(leading.length, convertedInner.length - trailing.length);
974
+ if (!content) return convertedInner;
975
+ if (tag === "sup" || tag === "sub") {
976
+ return `${leading}<${tag}>${content}</${tag}>${trailing}`;
977
+ }
978
+ const delimiter = tag === "strong" || tag === "b" ? "**" : tag === "em" || tag === "i" ? "*" : "~~";
979
+ return `${leading}${delimiter}${content}${delimiter}${trailing}`;
980
+ });
981
+ }
982
+ var RawHtmlVault = class {
983
+ constructor() {
984
+ this.parts = [];
985
+ }
986
+ /** Replace a raw HTML string with a sentinel the tag-stripper won't match. */
987
+ protect(html) {
988
+ this.parts.push(html);
989
+ return `\0RAW${this.parts.length - 1}\0`;
990
+ }
991
+ /** Swap every sentinel back for its raw HTML. */
992
+ restore(text) {
993
+ let out = text;
994
+ for (let i = this.parts.length - 1; i >= 0; i--) {
995
+ out = out.split(`\0RAW${i}\0`).join(this.parts[i]);
996
+ }
997
+ return out;
998
+ }
999
+ };
964
1000
  function htmlToInline(html) {
965
1001
  let result = html;
1002
+ const vault = new RawHtmlVault();
966
1003
  result = result.replace(/<br\s*\/?>/gi, " \n");
967
1004
  result = escapeMarkdownTextNodes(result);
968
1005
  result = result.replace(RE_ICON_TAG, (_m, token) => `{[${token}]}`);
969
- result = result.replace(RE_STRONG_TAG, "**$1**");
970
- result = result.replace(RE_B_TAG, "**$1**");
971
- result = result.replace(RE_EM_TAG, "*$1*");
972
- result = result.replace(RE_I_TAG, "*$1*");
973
- result = result.replace(RE_S_TAG, "~~$1~~");
974
- result = result.replace(RE_DEL_TAG, "~~$1~~");
1006
+ result = markdownFromMarkTags(result);
1007
+ result = result.replace(/<\/?(?:sup|sub)>/gi, (tag) => vault.protect(tag));
975
1008
  result = result.replace(RE_CODE_TAG, "`$1`");
976
1009
  result = result.replace(RE_MENTION_TAG, (match, _inner) => {
977
1010
  const kind = /data-kind="([^"]*)"/i.exec(match)?.[1] ?? "";
@@ -991,9 +1024,11 @@ function htmlToInline(html) {
991
1024
  const src = /\bsrc="([^"]*)"/i.exec(attrs)?.[1];
992
1025
  if (!src) return match;
993
1026
  const alt = /\balt="([^"]*)"/i.exec(attrs)?.[1] ?? "";
994
- return serializeImage(src, alt, attrs);
1027
+ const serialized = serializeImage(src, alt, attrs);
1028
+ return serialized.startsWith("<") ? vault.protect(serialized) : serialized;
995
1029
  });
996
1030
  result = result.replace(RE_STRIP_TAGS, "");
1031
+ result = vault.restore(result);
997
1032
  return restoreLeadingSpaces(unescapeHtml(result));
998
1033
  }
999
1034
  function preserveLeadingSpaces(html) {