@bendyline/squisq-editor-react 2.9.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-TLRB3IQC.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
+ };
@@ -20,8 +20,9 @@ var RE_MENTION = /@\[([^\]]+?)\]\(([a-z][a-z0-9+.-]*)\\?:([^)\s]+)\)/gi;
20
20
  var RE_MENTION_TAG = /<span\b[^>]*?\bdata-mention\b[^>]*?>(?:<[^>]+>)*([^<]*)<\/span>/gi;
21
21
  var RE_ICON_MD = /\{\[([a-zA-Z0-9_:-]+)\]\}/g;
22
22
  var RE_ICON_TAG = /<i\b[^>]*?\bdata-icon="([^"]*)"[^>]*?><\/i>/gi;
23
- var RE_MARK_TAG = /<(strong|b|em|i|s|del)>(.*?)<\/\1>/gs;
23
+ var RE_MARK_TAG = /<(strong|b|em|i|s|del|sup|sub)>(.*?)<\/\1>/gs;
24
24
  var RE_CODE_TAG = /<code>(.*?)<\/code>/g;
25
+ var RE_VERT_ALIGN_TAG = /<(sup|sub)>((?:(?!<\/?(?:sup|sub)>)[\s\S])*)<\/\1>/gi;
25
26
  var RE_A_TAG = /<a\b([^>]*)>(.*?)<\/a>/g;
26
27
  var RE_IMG_TAG = /<img\b([^>]*)>/g;
27
28
  var RE_STRIP_TAGS = /<[^>]+>/g;
@@ -784,6 +785,10 @@ function inlineToHtml(text) {
784
785
  RE_INLINE_CODE,
785
786
  (_m, code) => stash(`<code>${escapeHtml(code)}</code>`)
786
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
+ });
787
792
  staged = staged.replace(
788
793
  RE_IMAGE,
789
794
  (_m, alt, src) => stash(`<img alt="${escapeHtml(alt)}" src="${escapeHtml(src)}">`)
@@ -967,16 +972,39 @@ function markdownFromMarkTags(html) {
967
972
  const trailing = convertedInner.match(/\s*$/)?.[0] ?? "";
968
973
  const content = convertedInner.slice(leading.length, convertedInner.length - trailing.length);
969
974
  if (!content) return convertedInner;
975
+ if (tag === "sup" || tag === "sub") {
976
+ return `${leading}<${tag}>${content}</${tag}>${trailing}`;
977
+ }
970
978
  const delimiter = tag === "strong" || tag === "b" ? "**" : tag === "em" || tag === "i" ? "*" : "~~";
971
979
  return `${leading}${delimiter}${content}${delimiter}${trailing}`;
972
980
  });
973
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
+ };
974
1000
  function htmlToInline(html) {
975
1001
  let result = html;
1002
+ const vault = new RawHtmlVault();
976
1003
  result = result.replace(/<br\s*\/?>/gi, " \n");
977
1004
  result = escapeMarkdownTextNodes(result);
978
1005
  result = result.replace(RE_ICON_TAG, (_m, token) => `{[${token}]}`);
979
1006
  result = markdownFromMarkTags(result);
1007
+ result = result.replace(/<\/?(?:sup|sub)>/gi, (tag) => vault.protect(tag));
980
1008
  result = result.replace(RE_CODE_TAG, "`$1`");
981
1009
  result = result.replace(RE_MENTION_TAG, (match, _inner) => {
982
1010
  const kind = /data-kind="([^"]*)"/i.exec(match)?.[1] ?? "";
@@ -996,9 +1024,11 @@ function htmlToInline(html) {
996
1024
  const src = /\bsrc="([^"]*)"/i.exec(attrs)?.[1];
997
1025
  if (!src) return match;
998
1026
  const alt = /\balt="([^"]*)"/i.exec(attrs)?.[1] ?? "";
999
- return serializeImage(src, alt, attrs);
1027
+ const serialized = serializeImage(src, alt, attrs);
1028
+ return serialized.startsWith("<") ? vault.protect(serialized) : serialized;
1000
1029
  });
1001
1030
  result = result.replace(RE_STRIP_TAGS, "");
1031
+ result = vault.restore(result);
1002
1032
  return restoreLeadingSpaces(unescapeHtml(result));
1003
1033
  }
1004
1034
  function preserveLeadingSpaces(html) {