@mikedown/core 0.1.0 → 0.3.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.
Files changed (42) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +183 -3
  3. package/dist/ai/engines-page.d.ts +130 -0
  4. package/dist/ai/engines-page.js +10 -0
  5. package/dist/ai/engines-page.js.map +1 -0
  6. package/dist/ai/index.d.ts +1531 -0
  7. package/dist/ai/index.js +395 -0
  8. package/dist/ai/index.js.map +1 -0
  9. package/dist/ai-ui-C7ENTWWU.js +3370 -0
  10. package/dist/ai-ui-C7ENTWWU.js.map +1 -0
  11. package/dist/chunk-7A5HEPLK.js +4910 -0
  12. package/dist/chunk-7A5HEPLK.js.map +1 -0
  13. package/dist/chunk-GWCRPPCX.js +18 -0
  14. package/dist/chunk-GWCRPPCX.js.map +1 -0
  15. package/dist/chunk-JZWKEBCI.js +3899 -0
  16. package/dist/chunk-JZWKEBCI.js.map +1 -0
  17. package/dist/chunk-L4FO63DF.js +113 -0
  18. package/dist/chunk-L4FO63DF.js.map +1 -0
  19. package/dist/chunk-LXZG7IKN.js +39 -0
  20. package/dist/chunk-LXZG7IKN.js.map +1 -0
  21. package/dist/chunk-QDVY7WW6.js +165 -0
  22. package/dist/chunk-QDVY7WW6.js.map +1 -0
  23. package/dist/chunk-QWKT57TR.js +62 -0
  24. package/dist/chunk-QWKT57TR.js.map +1 -0
  25. package/dist/chunk-TSP7BAV3.js +22 -0
  26. package/dist/chunk-TSP7BAV3.js.map +1 -0
  27. package/dist/index.d.ts +62 -441
  28. package/dist/index.js +6871 -1247
  29. package/dist/index.js.map +1 -1
  30. package/dist/schema-DYzxkuSb.d.ts +46 -0
  31. package/dist/shared/anchoring.js +4 -16
  32. package/dist/shared/anchoring.js.map +1 -1
  33. package/dist/shared/imagePaste.js +3 -57
  34. package/dist/shared/imagePaste.js.map +1 -1
  35. package/dist/shared/sha1.js +3 -57
  36. package/dist/shared/sha1.js.map +1 -1
  37. package/dist/shared/wordCount.js +4 -12
  38. package/dist/shared/wordCount.js.map +1 -1
  39. package/dist/styles.css +3700 -94
  40. package/dist/types-CgEeIUhi.d.ts +432 -0
  41. package/dist/types-CsVNVSNY.d.ts +597 -0
  42. package/package.json +17 -2
@@ -0,0 +1,46 @@
1
+ import { h as AiEngineKind, A as AiSettings } from './types-CgEeIUhi.js';
2
+
3
+ /**
4
+ * `mikedown.ai.*` settings: parse (validate, fill defaults, keep unknown
5
+ * keys) and serialize back to a settings patch.
6
+ *
7
+ * `parseAiSettings` never throws. Malformed entries (an engine that is not
8
+ * an object or has no base URL, a model with no id, a route without both
9
+ * ids) are dropped with a warning; everything it does not recognize is kept
10
+ * as is, on every object, so a newer version's settings survive a save.
11
+ * Engines without an id get one, once: the id is written back by
12
+ * `serializeAiSettings`, so it is stable from then on.
13
+ */
14
+
15
+ interface ParseAiSettingsOptions {
16
+ /** Id for an engine that has none. Default: a slug of its name plus random characters. */
17
+ generateId?: (name: string) => string;
18
+ /** Called once per dropped or repaired entry. Default `console.warn`. */
19
+ onWarning?: (message: string) => void;
20
+ }
21
+ /** The top-level keys, without the `ai.` prefix. */
22
+ declare const AI_SETTINGS_KEYS: readonly ["enabled", "allowCloud", "engines", "routing", "features", "localNetworks", "showToolbarButton"];
23
+
24
+ declare function isKnownEngineKind(kind: string): kind is AiEngineKind;
25
+ /** A kind only one host runs, with no address or key (`vscode-lm`); see `AI_HOST_ENGINE_KINDS`. */
26
+ declare function isHostEngineKind(kind: string): boolean;
27
+ /** A new engine id: `omlx-mac-studio-k3f9a2`. Never shown, only has to be unique and stable. */
28
+ declare function generateAiId(name?: string): string;
29
+ /**
30
+ * Read `raw` as AI settings. Accepts the nested object VS Code returns for
31
+ * `getConfiguration('mikedown.ai')` (`{ enabled, engines, ... }`) or a flat
32
+ * store with `ai.`-prefixed keys (Desktop, a settings patch). Anything else,
33
+ * including undefined, gives the defaults.
34
+ */
35
+ declare function parseAiSettings(raw: unknown, opts?: ParseAiSettingsOptions): AiSettings;
36
+ /** The settings patch keys, `ai.`-prefixed: what the editor sends to `host.saveSettings`. */
37
+ type AiSettingsPatch = Record<`ai.${string}`, unknown>;
38
+ /**
39
+ * Settings to a `saveSettings` patch: one `ai.<key>` per top-level key,
40
+ * including unknown ones, as plain JSON (deep copies). VS Code writes each as
41
+ * `mikedown.ai.<key>`; Desktop stores the flat keys. Secrets are never here.
42
+ */
43
+ declare function serializeAiSettings(settings: AiSettings): AiSettingsPatch;
44
+
45
+ export { AI_SETTINGS_KEYS as a, isKnownEngineKind as b, generateAiId as g, isHostEngineKind as i, parseAiSettings as p, serializeAiSettings as s };
46
+ export type { AiSettingsPatch as A, ParseAiSettingsOptions as P };
@@ -1,19 +1,7 @@
1
- // src/shared/anchoring.ts
2
- function githubAnchorId(text) {
3
- return text.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/\s/g, "-");
4
- }
5
- function dedupedAnchorIdAt(headingTexts, index) {
6
- const seen = /* @__PURE__ */ new Map();
7
- for (let i = 0; i < headingTexts.length; i++) {
8
- const base = githubAnchorId(headingTexts[i]);
9
- const count = seen.get(base) ?? 0;
10
- seen.set(base, count + 1);
11
- if (i === index) {
12
- return count === 0 ? base : `${base}-${count}`;
13
- }
14
- }
15
- return "";
16
- }
1
+ import {
2
+ dedupedAnchorIdAt,
3
+ githubAnchorId
4
+ } from "../chunk-TSP7BAV3.js";
17
5
  export {
18
6
  dedupedAnchorIdAt,
19
7
  githubAnchorId
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/shared/anchoring.ts"],"sourcesContent":["// Shared GitHub-style anchor slug generation.\n//\n// IMPORTANT: this module must be importable by BOTH webpack bundles — the\n// extension host (node target, tsconfig.json) and the webview (web target,\n// tsconfig.webview.json). Keep it dependency-free: no `vscode`, no `node:*`,\n// no DOM. (Same rule as imageDisplayPath.ts.) The \"Heading Rename → Fix Links\"\n// feature relies on host and webview computing byte-identical slugs.\n\n/**\n * Convert heading text to a GitHub-style anchor id (the \"base\" slug, before any\n * duplicate-disambiguation suffix).\n *\n * Match GitHub exactly: strip punctuation, then turn each whitespace char into a\n * hyphen. Do NOT collapse consecutive hyphens or trim trailing ones — GitHub\n * keeps them (e.g. \"Memory & Hardware\" → \"memory--hardware\", \"UD-\" → \"ud-\").\n */\nexport function githubAnchorId(text: string): string {\n return text\n .toLowerCase()\n .trim()\n .replace(/[^\\w\\s-]/g, '') // remove punctuation (keep word chars, whitespace, hyphens)\n .replace(/\\s/g, '-'); // each whitespace char → hyphen (preserve consecutive)\n}\n\n/**\n * Given heading texts in document order, return the deduplicated anchor id for\n * the heading at `index`, using GitHub's encounter-order suffixing\n * (`setup`, `setup-1`, `setup-2`, …). The first occurrence keeps the base slug.\n */\nexport function dedupedAnchorIdAt(headingTexts: string[], index: number): string {\n const seen = new Map<string, number>();\n for (let i = 0; i < headingTexts.length; i++) {\n const base = githubAnchorId(headingTexts[i]);\n const count = seen.get(base) ?? 0;\n seen.set(base, count + 1);\n if (i === index) {return count === 0 ? base : `${base}-${count}`;}\n }\n return '';\n}\n"],"mappings":";AAgBO,SAAS,eAAe,MAAsB;AACnD,SAAO,KACJ,YAAY,EACZ,KAAK,EACL,QAAQ,aAAa,EAAE,EACvB,QAAQ,OAAO,GAAG;AACvB;AAOO,SAAS,kBAAkB,cAAwB,OAAuB;AAC/E,QAAM,OAAO,oBAAI,IAAoB;AACrC,WAAS,IAAI,GAAG,IAAI,aAAa,QAAQ,KAAK;AAC5C,UAAM,OAAO,eAAe,aAAa,CAAC,CAAC;AAC3C,UAAM,QAAQ,KAAK,IAAI,IAAI,KAAK;AAChC,SAAK,IAAI,MAAM,QAAQ,CAAC;AACxB,QAAI,MAAM,OAAO;AAAC,aAAO,UAAU,IAAI,OAAO,GAAG,IAAI,IAAI,KAAK;AAAA,IAAG;AAAA,EACnE;AACA,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -1,60 +1,6 @@
1
- // src/shared/sha1.ts
2
- function rotl(x, n) {
3
- return x << n | x >>> 32 - n;
4
- }
5
- function sha1Hex(data) {
6
- const len = data.length;
7
- const padded = new Uint8Array((len + 8 >> 6) + 1 << 6);
8
- padded.set(data);
9
- padded[len] = 128;
10
- const view = new DataView(padded.buffer);
11
- view.setUint32(padded.length - 8, Math.floor(len / 536870912));
12
- view.setUint32(padded.length - 4, len * 8 >>> 0);
13
- let h0 = 1732584193;
14
- let h1 = 4023233417;
15
- let h2 = 2562383102;
16
- let h3 = 271733878;
17
- let h4 = 3285377520;
18
- const w = new Uint32Array(80);
19
- for (let off = 0; off < padded.length; off += 64) {
20
- for (let i = 0; i < 16; i++) {
21
- w[i] = view.getUint32(off + i * 4);
22
- }
23
- for (let i = 16; i < 80; i++) {
24
- w[i] = rotl(w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16], 1);
25
- }
26
- let a = h0, b = h1, c = h2, d = h3, e = h4;
27
- for (let i = 0; i < 80; i++) {
28
- let f;
29
- let k;
30
- if (i < 20) {
31
- f = b & c | ~b & d;
32
- k = 1518500249;
33
- } else if (i < 40) {
34
- f = b ^ c ^ d;
35
- k = 1859775393;
36
- } else if (i < 60) {
37
- f = b & c | b & d | c & d;
38
- k = 2400959708;
39
- } else {
40
- f = b ^ c ^ d;
41
- k = 3395469782;
42
- }
43
- const t = rotl(a, 5) + f + e + k + w[i] >>> 0;
44
- e = d;
45
- d = c;
46
- c = rotl(b, 30) >>> 0;
47
- b = a;
48
- a = t;
49
- }
50
- h0 = h0 + a >>> 0;
51
- h1 = h1 + b >>> 0;
52
- h2 = h2 + c >>> 0;
53
- h3 = h3 + d >>> 0;
54
- h4 = h4 + e >>> 0;
55
- }
56
- return [h0, h1, h2, h3, h4].map((h) => h.toString(16).padStart(8, "0")).join("");
57
- }
1
+ import {
2
+ sha1Hex
3
+ } from "../chunk-QWKT57TR.js";
58
4
 
59
5
  // src/shared/imagePaste.ts
60
6
  var MIME_TO_EXT = {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/shared/sha1.ts","../../src/shared/imagePaste.ts"],"sourcesContent":["// Pure, synchronous SHA-1 over bytes. No node:crypto, so it runs in the\n// webview, a Tauri webview, and Node alike. Used only for the image-paste\n// `${hash}` token and hash-dedupe (content fingerprinting, not security);\n// hosts with a native hash (Node `crypto`) can inject theirs instead.\n\nfunction rotl(x: number, n: number): number {\n return (x << n) | (x >>> (32 - n));\n}\n\nexport function sha1Hex(data: Uint8Array): string {\n const len = data.length;\n const padded = new Uint8Array((((len + 8) >> 6) + 1) << 6);\n padded.set(data);\n padded[len] = 0x80;\n const view = new DataView(padded.buffer);\n // 64-bit big-endian bit length.\n view.setUint32(padded.length - 8, Math.floor(len / 0x20000000));\n view.setUint32(padded.length - 4, (len * 8) >>> 0);\n\n let h0 = 0x67452301;\n let h1 = 0xefcdab89;\n let h2 = 0x98badcfe;\n let h3 = 0x10325476;\n let h4 = 0xc3d2e1f0;\n const w = new Uint32Array(80);\n\n for (let off = 0; off < padded.length; off += 64) {\n for (let i = 0; i < 16; i++) {w[i] = view.getUint32(off + i * 4);}\n for (let i = 16; i < 80; i++) {w[i] = rotl(w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16], 1);}\n let a = h0, b = h1, c = h2, d = h3, e = h4;\n for (let i = 0; i < 80; i++) {\n let f: number;\n let k: number;\n if (i < 20) { f = (b & c) | (~b & d); k = 0x5a827999; }\n else if (i < 40) { f = b ^ c ^ d; k = 0x6ed9eba1; }\n else if (i < 60) { f = (b & c) | (b & d) | (c & d); k = 0x8f1bbcdc; }\n else { f = b ^ c ^ d; k = 0xca62c1d6; }\n const t = (rotl(a, 5) + f + e + k + w[i]) >>> 0;\n e = d; d = c; c = rotl(b, 30) >>> 0; b = a; a = t;\n }\n h0 = (h0 + a) >>> 0;\n h1 = (h1 + b) >>> 0;\n h2 = (h2 + c) >>> 0;\n h3 = (h3 + d) >>> 0;\n h4 = (h4 + e) >>> 0;\n }\n return [h0, h1, h2, h3, h4].map(h => h.toString(16).padStart(8, '0')).join('');\n}\n","// Image-paste helpers shared by the VS Code extension host and the desktop\n// app. Platform-neutral: no Node `path`/`crypto` imports. Path-dependent\n// helpers come from `createImagePasteHelpers(env)`, where the host injects its\n// path implementation (Node `path` in the extension) and optionally a SHA-1\n// function (defaults to the pure `sha1Hex` in `./sha1`).\nimport { sha1Hex as pureSha1Hex } from './sha1';\n\nexport interface ImagePasteSettings {\n enabled: boolean;\n folder: string;\n folderRelativeTo: 'document' | 'workspace';\n filenamePattern: string;\n pathStyle: 'relative' | 'workspace-absolute';\n altText: 'empty' | 'filename' | 'prompt';\n maxSizeMB: number;\n cleanupUnreferenced: boolean;\n}\n\n/** The subset of Node's `path` module these helpers use. */\nexport interface PathOps {\n sep: string;\n join(...parts: string[]): string;\n resolve(...parts: string[]): string;\n dirname(p: string): string;\n basename(p: string, ext?: string): string;\n extname(p: string): string;\n relative(from: string, to: string): string;\n isAbsolute(p: string): boolean;\n normalize(p: string): string;\n}\n\nexport interface ImagePasteEnv {\n path: PathOps;\n /** Full lowercase hex SHA-1 of the bytes. Defaults to the pure implementation. */\n sha1Hex?: (data: Uint8Array) => string;\n}\n\nconst MIME_TO_EXT: Record<string, string> = {\n 'image/png': 'png',\n 'image/jpeg': 'jpg',\n 'image/jpg': 'jpg',\n 'image/gif': 'gif',\n 'image/webp': 'webp',\n 'image/svg+xml': 'svg',\n 'image/bmp': 'bmp',\n 'image/x-icon': 'ico',\n 'image/tiff': 'tiff',\n 'image/avif': 'avif',\n};\n\nexport function extensionFromMime(mime: string): string | null {\n return MIME_TO_EXT[mime.toLowerCase()] ?? null;\n}\n\nexport interface FilenameContext {\n /** Markdown document base name without extension (e.g. \"my-note\"). */\n docName: string;\n /** Output extension without dot (e.g. \"png\"). */\n extension: string;\n /** Image bytes — used for the `${hash}` token. */\n data: Uint8Array;\n /** Timestamp used for `${date}`/`${time}`/`${timestamp}` tokens. */\n timestamp: Date;\n /** Auto-increment value for the `${index}` token. Defaults to 1. */\n index?: number;\n}\n\n/**\n * Strip characters that don't belong in a filename (Windows-strict set + control chars).\n * Collapses runs of whitespace/separators to a single dash.\n */\nexport function sanitizeForFilename(input: string): string {\n return input\n // eslint-disable-next-line no-control-regex -- intentional: strips ASCII control chars (\\x00-\\x1f) from filenames\n .replace(/[<>:\"/\\\\|?*\\x00-\\x1f]/g, '')\n .replace(/\\s+/g, '-')\n .replace(/-+/g, '-')\n .replace(/^[.-]+|[.-]+$/g, '');\n}\n\nexport interface FilenameResolution {\n /** Final absolute path to the image on disk. */\n absPath: string;\n /** True when an existing identical file was reused (hash-dedupe hit). */\n reused: boolean;\n}\n\n/**\n * Pull all `![alt](src)` matches out of a markdown string and return their\n * `src` values. Skips external (http/https) and data URIs since those don't\n * map to local files we'd ever want to delete.\n */\nexport function extractLocalImageRefs(markdown: string): string[] {\n const out: string[] = [];\n const re = /!\\[([^\\]]*)\\]\\(([^)]+)\\)/g;\n let m: RegExpExecArray | null;\n while ((m = re.exec(markdown)) !== null) {\n const src = m[2].trim();\n if (!src) {continue;}\n if (/^(?:[a-z][a-z0-9+.-]*:|data:|\\/\\/)/i.test(src) && !src.startsWith('/')) {continue;}\n // Strip cache-bust query/fragment that the resize popover may have left in.\n const queryStart = src.search(/[?#]/);\n out.push(queryStart >= 0 ? src.slice(0, queryStart) : src);\n }\n return out;\n}\n\n/**\n * Heuristic: does `filename` look like a MikeDown-generated paste filename\n * (rather than a user-curated asset like `logo.png`)?\n *\n * Builds a regex from the configured `filenamePattern` by replacing tokens\n * with shape-anchors (`${timestamp}` → `\\d{8}-\\d{6}`, `${hash}` → 8 hex chars,\n * etc.). Allows an optional `-N` collision suffix and any image extension.\n *\n * Returns false on any pattern parse failure so we never accidentally widen\n * the cleanup set when the user has set a weird custom pattern.\n */\nexport function looksLikeAutoPastedImage(\n filename: string,\n pattern: string,\n docName: string\n): boolean {\n if (!pattern) {return false;}\n const sanitizedDoc = sanitizeForFilename(docName) || 'image';\n const escapeRegex = (s: string): string => s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n\n let regex = '^';\n let i = 0;\n while (i < pattern.length) {\n const tail = pattern.slice(i);\n const tokenMatch = /^\\$\\{(\\w+)\\}/.exec(tail);\n if (tokenMatch) {\n const key = tokenMatch[1];\n switch (key) {\n case 'docName': regex += escapeRegex(sanitizedDoc); break;\n case 'date': regex += '\\\\d{8}'; break;\n case 'time': regex += '\\\\d{6}'; break;\n case 'timestamp': regex += '\\\\d{8}-\\\\d{6}'; break;\n case 'hash': regex += '[0-9a-f]{8}'; break;\n case 'index': regex += '\\\\d+'; break;\n // Unknown token — bail rather than match too broadly.\n default: return false;\n }\n i += tokenMatch[0].length;\n } else {\n regex += escapeRegex(pattern[i]);\n i += 1;\n }\n }\n regex += '(?:-\\\\d+)?\\\\.[a-zA-Z0-9]+$';\n try {\n return new RegExp(regex).test(filename);\n } catch {\n return false;\n }\n}\n\n/**\n * Decide what alt text to use, given the configured strategy and (optionally)\n * a user-provided value from the prompt UI.\n */\nexport function resolveAltText(\n strategy: 'empty' | 'filename' | 'prompt',\n filenameNoExt: string,\n promptedValue: string | undefined\n): string {\n if (strategy === 'filename') {return filenameNoExt;}\n if (strategy === 'prompt') {return promptedValue ?? '';}\n return '';\n}\n\n/**\n * Build the path- and hash-dependent helpers for one host. The extension\n * passes Node's `path` and a `crypto` SHA-1; the desktop app passes its own\n * synchronous path implementation (the pure SHA-1 default works there).\n */\nexport function createImagePasteHelpers(env: ImagePasteEnv) {\n const path = env.path;\n const hashImpl = env.sha1Hex ?? pureSha1Hex;\n\n /**\n * Replace pattern tokens with concrete values. Unknown tokens pass through\n * untouched so user-typed text stays intact.\n *\n * Supported tokens:\n * ${docName} sanitized document base name\n * ${date} YYYYMMDD\n * ${time} HHmmss\n * ${timestamp} YYYYMMDD-HHmmss\n * ${hash} 8-char SHA-1 of the image bytes\n * ${index} auto-increment value (default 1)\n */\n function expandFilenamePattern(pattern: string, ctx: FilenameContext): string {\n const pad = (n: number, w = 2): string => String(n).padStart(w, '0');\n const t = ctx.timestamp;\n const date = `${t.getFullYear()}${pad(t.getMonth() + 1)}${pad(t.getDate())}`;\n const time = `${pad(t.getHours())}${pad(t.getMinutes())}${pad(t.getSeconds())}`;\n const timestamp = `${date}-${time}`;\n\n // Lazy: only compute the hash if the pattern actually uses it.\n let hash = '';\n if (pattern.includes('${hash}')) {\n hash = sha1Hex(ctx.data).slice(0, 8);\n }\n\n const tokens: Record<string, string> = {\n docName: sanitizeForFilename(ctx.docName) || 'image',\n date,\n time,\n timestamp,\n hash,\n index: String(ctx.index ?? 1),\n };\n\n return pattern.replace(/\\$\\{(\\w+)\\}/g, (match: string, key: string) => {\n return Object.prototype.hasOwnProperty.call(tokens, key) ? tokens[key] : match;\n });\n }\n\n function sha1Hex(data: Uint8Array): string {\n return hashImpl(data);\n }\n\n /**\n * Resolve the absolute folder where pasted images should be written.\n * `workspaceRoot` may be undefined when the document is outside any workspace —\n * in that case `folderRelativeTo === 'workspace'` falls back to the document's\n * own directory.\n */\n function resolveTargetFolder(\n config: ImagePasteSettings,\n documentPath: string,\n workspaceRoot: string | undefined\n ): string {\n const folder = expandFolderPattern(config.folder, documentPath);\n if (path.isAbsolute(folder)) {\n return folder;\n }\n const base = config.folderRelativeTo === 'workspace' && workspaceRoot\n ? workspaceRoot\n : path.dirname(documentPath);\n return path.resolve(base, folder);\n }\n\n /**\n * Expand `${docName}` in the configured folder so each document can get its\n * own image folder (e.g. `assets/${docName}`). Only `${docName}` is supported:\n * time-based tokens would scatter one document's images across folders and\n * break orphan-cleanup scoping. Other tokens pass through untouched.\n */\n function expandFolderPattern(folder: string, documentPath: string): string {\n const docName = path.basename(documentPath, path.extname(documentPath));\n const safe = sanitizeForFilename(docName) || 'image';\n return folder.replace(/\\$\\{docName\\}/g, safe);\n }\n\n /**\n * Compute the final on-disk path, handling collisions and hash-dedupe.\n *\n * Behavior:\n * - If the resolved name (with extension) doesn't exist → use it verbatim.\n * - If it exists AND `${hash}` is in the pattern AND content hashes match\n * (per `readExistingHash`) → reuse the existing file (`reused: true`).\n * - Otherwise → append `-1`, `-2`, … until a free slot is found.\n *\n * `readExistingHash` is injected so this function stays pure / testable; pass\n * a real fs-backed implementation from the production caller.\n */\n function resolveFilename(\n pattern: string,\n targetFolder: string,\n ctx: FilenameContext,\n fileExists: (absPath: string) => boolean,\n readExistingHash?: (absPath: string) => string\n ): FilenameResolution {\n const ext = ctx.extension;\n const baseName = expandFilenamePattern(pattern, ctx);\n const initialPath = path.join(targetFolder, `${baseName}.${ext}`);\n\n if (!fileExists(initialPath)) {\n return { absPath: initialPath, reused: false };\n }\n\n // Hash-dedupe: only triggered when the pattern explicitly opts in via ${hash}.\n // Without that opt-in we always create a new file (collision suffix path).\n if (pattern.includes('${hash}') && readExistingHash) {\n const incomingHash = sha1Hex(ctx.data);\n const existingHash = readExistingHash(initialPath);\n if (existingHash === incomingHash) {\n return { absPath: initialPath, reused: true };\n }\n }\n\n let i = 1;\n while (true) {\n const candidate = path.join(targetFolder, `${baseName}-${i}.${ext}`);\n if (!fileExists(candidate)) {\n return { absPath: candidate, reused: false };\n }\n i += 1;\n }\n }\n\n /**\n * Produce the path string to write into the markdown image link.\n *\n * `relative` — POSIX-relative from the document's directory\n * (e.g. `images/foo.png`). Anchors with `./` only when\n * the path would otherwise look like a sibling file\n * starting with a letter — leaving plain `images/foo.png`\n * is the typical convention and what users expect.\n * `workspace-absolute` — leading `/`, POSIX-relative from the workspace root.\n * Falls back to the document-relative form when the\n * document is outside any workspace.\n */\n function formatInsertPath(\n absImagePath: string,\n documentPath: string,\n workspaceRoot: string | undefined,\n pathStyle: 'relative' | 'workspace-absolute'\n ): string {\n const docDir = path.dirname(documentPath);\n if (pathStyle === 'workspace-absolute' && workspaceRoot) {\n return '/' + toPosix(path.relative(workspaceRoot, absImagePath));\n }\n return toPosix(path.relative(docDir, absImagePath));\n }\n\n function toPosix(p: string): string {\n return p.split(path.sep).join('/');\n }\n\n /**\n * Compute a sibling filename for a resized variant (e.g. `foo.png` + 50 →\n * `foo-50pct.png`). If that sibling already exists, append `-1`, `-2`, …\n * until a free slot is found.\n */\n function resizeSiblingPath(\n absPath: string,\n percent: number,\n fileExists: (absPath: string) => boolean\n ): string {\n const dir = path.dirname(absPath);\n const ext = path.extname(absPath);\n const base = path.basename(absPath, ext);\n const stem = `${base}-${Math.round(percent)}pct`;\n const initial = path.join(dir, `${stem}${ext}`);\n if (!fileExists(initial)) {return initial;}\n let i = 1;\n while (true) {\n const candidate = path.join(dir, `${stem}-${i}${ext}`);\n if (!fileExists(candidate)) {return candidate;}\n i += 1;\n }\n }\n\n /**\n * Resolve a list of image-link srcs (as parsed from markdown) to absolute fs\n * paths. `documentDir` is the directory of the markdown file the srcs came\n * from. `workspaceRoot` is required to resolve workspace-absolute paths\n * (those starting with `/`).\n */\n function resolveLocalImagePaths(\n srcs: string[],\n documentDir: string,\n workspaceRoot: string | undefined\n ): string[] {\n const out: string[] = [];\n for (const src of srcs) {\n if (path.isAbsolute(src)) {\n // A leading-slash path is workspace-absolute when we have a workspace,\n // otherwise it's a real absolute path on the host filesystem.\n if (src.startsWith('/') && workspaceRoot) {\n out.push(path.join(workspaceRoot, src));\n } else {\n out.push(src);\n }\n continue;\n }\n out.push(path.resolve(documentDir, src));\n }\n return out;\n }\n\n /**\n * True iff `absImagePath` lives inside the resolved imagePaste folder for\n * this document. Used by orphan-cleanup to scope deletion to MikeDown-managed\n * locations (we never touch images the user keeps elsewhere).\n */\n function isInsideManagedFolder(\n absImagePath: string,\n config: ImagePasteSettings,\n documentPath: string,\n workspaceRoot: string | undefined\n ): boolean {\n const folder = resolveTargetFolder(config, documentPath, workspaceRoot);\n // Use trailing-separator match so `/foo/imagesX/y.png` doesn't count as\n // being inside `/foo/images`.\n const folderWithSep = folder.endsWith(path.sep) ? folder : folder + path.sep;\n return absImagePath === folder || absImagePath.startsWith(folderWithSep);\n }\n\n /**\n * True iff `childPath` is `parentDir` itself or lives somewhere underneath it.\n * Uses `path.relative` so this is correct across separators; a relative\n * result that starts with `..` (or that is itself absolute, which\n * `path.relative` returns for cross-drive paths on Windows) means `childPath`\n * is outside `parentDir`.\n */\n function isPathInside(parentDir: string, childPath: string): boolean {\n const rel = path.relative(parentDir, childPath);\n return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));\n }\n\n /**\n * True iff `absPath` is outside every directory in `roots`. Used by the image\n * file picker (M4) to decide whether a picked file needs to be copied into\n * the image-paste target folder to be servable — VS Code's webview\n * `localResourceRoots` only covers the extension's own directories, the\n * document's folder, and workspace folders, so a file picked from elsewhere\n * (e.g. the user's Desktop) won't render unless it's copied in. Files already\n * inside one of `roots` are referenced in place.\n */\n function isOutsideResourceRoots(absPath: string, roots: string[]): boolean {\n return !roots.some(root => isPathInside(root, absPath));\n }\n\n return {\n expandFilenamePattern,\n resolveTargetFolder,\n expandFolderPattern,\n resolveFilename,\n formatInsertPath,\n resizeSiblingPath,\n resolveLocalImagePaths,\n isInsideManagedFolder,\n isPathInside,\n isOutsideResourceRoots,\n extensionFromMime,\n sanitizeForFilename,\n extractLocalImageRefs,\n looksLikeAutoPastedImage,\n resolveAltText,\n };\n}\n\nexport type ImagePasteHelpers = ReturnType<typeof createImagePasteHelpers>;\n"],"mappings":";AAKA,SAAS,KAAK,GAAW,GAAmB;AAC1C,SAAQ,KAAK,IAAM,MAAO,KAAK;AACjC;AAEO,SAAS,QAAQ,MAA0B;AAChD,QAAM,MAAM,KAAK;AACjB,QAAM,SAAS,IAAI,YAAc,MAAM,KAAM,KAAK,KAAM,CAAC;AACzD,SAAO,IAAI,IAAI;AACf,SAAO,GAAG,IAAI;AACd,QAAM,OAAO,IAAI,SAAS,OAAO,MAAM;AAEvC,OAAK,UAAU,OAAO,SAAS,GAAG,KAAK,MAAM,MAAM,SAAU,CAAC;AAC9D,OAAK,UAAU,OAAO,SAAS,GAAI,MAAM,MAAO,CAAC;AAEjD,MAAI,KAAK;AACT,MAAI,KAAK;AACT,MAAI,KAAK;AACT,MAAI,KAAK;AACT,MAAI,KAAK;AACT,QAAM,IAAI,IAAI,YAAY,EAAE;AAE5B,WAAS,MAAM,GAAG,MAAM,OAAO,QAAQ,OAAO,IAAI;AAChD,aAAS,IAAI,GAAG,IAAI,IAAI,KAAK;AAAC,QAAE,CAAC,IAAI,KAAK,UAAU,MAAM,IAAI,CAAC;AAAA,IAAE;AACjE,aAAS,IAAI,IAAI,IAAI,IAAI,KAAK;AAAC,QAAE,CAAC,IAAI,KAAK,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,CAAC;AAAA,IAAE;AAC3F,QAAI,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI;AACxC,aAAS,IAAI,GAAG,IAAI,IAAI,KAAK;AAC3B,UAAI;AACJ,UAAI;AACJ,UAAI,IAAI,IAAI;AAAE,YAAK,IAAI,IAAM,CAAC,IAAI;AAAI,YAAI;AAAA,MAAY,WAC7C,IAAI,IAAI;AAAE,YAAI,IAAI,IAAI;AAAG,YAAI;AAAA,MAAY,WACzC,IAAI,IAAI;AAAE,YAAK,IAAI,IAAM,IAAI,IAAM,IAAI;AAAI,YAAI;AAAA,MAAY,OAC/D;AAAE,YAAI,IAAI,IAAI;AAAG,YAAI;AAAA,MAAY;AACtC,YAAM,IAAK,KAAK,GAAG,CAAC,IAAI,IAAI,IAAI,IAAI,EAAE,CAAC,MAAO;AAC9C,UAAI;AAAG,UAAI;AAAG,UAAI,KAAK,GAAG,EAAE,MAAM;AAAG,UAAI;AAAG,UAAI;AAAA,IAClD;AACA,SAAM,KAAK,MAAO;AAClB,SAAM,KAAK,MAAO;AAClB,SAAM,KAAK,MAAO;AAClB,SAAM,KAAK,MAAO;AAClB,SAAM,KAAK,MAAO;AAAA,EACpB;AACA,SAAO,CAAC,IAAI,IAAI,IAAI,IAAI,EAAE,EAAE,IAAI,OAAK,EAAE,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG,CAAC,EAAE,KAAK,EAAE;AAC/E;;;ACVA,IAAM,cAAsC;AAAA,EAC1C,aAAa;AAAA,EACb,cAAc;AAAA,EACd,aAAa;AAAA,EACb,aAAa;AAAA,EACb,cAAc;AAAA,EACd,iBAAiB;AAAA,EACjB,aAAa;AAAA,EACb,gBAAgB;AAAA,EAChB,cAAc;AAAA,EACd,cAAc;AAChB;AAEO,SAAS,kBAAkB,MAA6B;AAC7D,SAAO,YAAY,KAAK,YAAY,CAAC,KAAK;AAC5C;AAmBO,SAAS,oBAAoB,OAAuB;AACzD,SAAO,MAEJ,QAAQ,0BAA0B,EAAE,EACpC,QAAQ,QAAQ,GAAG,EACnB,QAAQ,OAAO,GAAG,EAClB,QAAQ,kBAAkB,EAAE;AACjC;AAcO,SAAS,sBAAsB,UAA4B;AAChE,QAAM,MAAgB,CAAC;AACvB,QAAM,KAAK;AACX,MAAI;AACJ,UAAQ,IAAI,GAAG,KAAK,QAAQ,OAAO,MAAM;AACvC,UAAM,MAAM,EAAE,CAAC,EAAE,KAAK;AACtB,QAAI,CAAC,KAAK;AAAC;AAAA,IAAS;AACpB,QAAI,sCAAsC,KAAK,GAAG,KAAK,CAAC,IAAI,WAAW,GAAG,GAAG;AAAC;AAAA,IAAS;AAEvF,UAAM,aAAa,IAAI,OAAO,MAAM;AACpC,QAAI,KAAK,cAAc,IAAI,IAAI,MAAM,GAAG,UAAU,IAAI,GAAG;AAAA,EAC3D;AACA,SAAO;AACT;AAaO,SAAS,yBACd,UACA,SACA,SACS;AACT,MAAI,CAAC,SAAS;AAAC,WAAO;AAAA,EAAM;AAC5B,QAAM,eAAe,oBAAoB,OAAO,KAAK;AACrD,QAAM,cAAc,CAAC,MAAsB,EAAE,QAAQ,uBAAuB,MAAM;AAElF,MAAI,QAAQ;AACZ,MAAI,IAAI;AACR,SAAO,IAAI,QAAQ,QAAQ;AACzB,UAAM,OAAO,QAAQ,MAAM,CAAC;AAC5B,UAAM,aAAa,eAAe,KAAK,IAAI;AAC3C,QAAI,YAAY;AACd,YAAM,MAAM,WAAW,CAAC;AACxB,cAAQ,KAAK;AAAA,QACX,KAAK;AAAW,mBAAS,YAAY,YAAY;AAAG;AAAA,QACpD,KAAK;AAAQ,mBAAS;AAAU;AAAA,QAChC,KAAK;AAAQ,mBAAS;AAAU;AAAA,QAChC,KAAK;AAAa,mBAAS;AAAiB;AAAA,QAC5C,KAAK;AAAQ,mBAAS;AAAe;AAAA,QACrC,KAAK;AAAS,mBAAS;AAAQ;AAAA;AAAA,QAE/B;AAAS,iBAAO;AAAA,MAClB;AACA,WAAK,WAAW,CAAC,EAAE;AAAA,IACrB,OAAO;AACL,eAAS,YAAY,QAAQ,CAAC,CAAC;AAC/B,WAAK;AAAA,IACP;AAAA,EACF;AACA,WAAS;AACT,MAAI;AACF,WAAO,IAAI,OAAO,KAAK,EAAE,KAAK,QAAQ;AAAA,EACxC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAMO,SAAS,eACd,UACA,eACA,eACQ;AACR,MAAI,aAAa,YAAY;AAAC,WAAO;AAAA,EAAc;AACnD,MAAI,aAAa,UAAU;AAAC,WAAO,iBAAiB;AAAA,EAAG;AACvD,SAAO;AACT;AAOO,SAAS,wBAAwB,KAAoB;AAC1D,QAAM,OAAO,IAAI;AACjB,QAAM,WAAW,IAAI,WAAW;AAchC,WAAS,sBAAsB,SAAiB,KAA8B;AAC5E,UAAM,MAAM,CAAC,GAAW,IAAI,MAAc,OAAO,CAAC,EAAE,SAAS,GAAG,GAAG;AACnE,UAAM,IAAI,IAAI;AACd,UAAM,OAAO,GAAG,EAAE,YAAY,CAAC,GAAG,IAAI,EAAE,SAAS,IAAI,CAAC,CAAC,GAAG,IAAI,EAAE,QAAQ,CAAC,CAAC;AAC1E,UAAM,OAAO,GAAG,IAAI,EAAE,SAAS,CAAC,CAAC,GAAG,IAAI,EAAE,WAAW,CAAC,CAAC,GAAG,IAAI,EAAE,WAAW,CAAC,CAAC;AAC7E,UAAM,YAAY,GAAG,IAAI,IAAI,IAAI;AAGjC,QAAI,OAAO;AACX,QAAI,QAAQ,SAAS,SAAS,GAAG;AAC/B,aAAOA,SAAQ,IAAI,IAAI,EAAE,MAAM,GAAG,CAAC;AAAA,IACrC;AAEA,UAAM,SAAiC;AAAA,MACrC,SAAS,oBAAoB,IAAI,OAAO,KAAK;AAAA,MAC7C;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,OAAO,OAAO,IAAI,SAAS,CAAC;AAAA,IAC9B;AAEA,WAAO,QAAQ,QAAQ,gBAAgB,CAAC,OAAe,QAAgB;AACrE,aAAO,OAAO,UAAU,eAAe,KAAK,QAAQ,GAAG,IAAI,OAAO,GAAG,IAAI;AAAA,IAC3E,CAAC;AAAA,EACH;AAEA,WAASA,SAAQ,MAA0B;AACzC,WAAO,SAAS,IAAI;AAAA,EACtB;AAQA,WAAS,oBACP,QACA,cACA,eACQ;AACR,UAAM,SAAS,oBAAoB,OAAO,QAAQ,YAAY;AAC9D,QAAI,KAAK,WAAW,MAAM,GAAG;AAC3B,aAAO;AAAA,IACT;AACA,UAAM,OAAO,OAAO,qBAAqB,eAAe,gBACpD,gBACA,KAAK,QAAQ,YAAY;AAC7B,WAAO,KAAK,QAAQ,MAAM,MAAM;AAAA,EAClC;AAQA,WAAS,oBAAoB,QAAgB,cAA8B;AACzE,UAAM,UAAU,KAAK,SAAS,cAAc,KAAK,QAAQ,YAAY,CAAC;AACtE,UAAM,OAAO,oBAAoB,OAAO,KAAK;AAC7C,WAAO,OAAO,QAAQ,kBAAkB,IAAI;AAAA,EAC9C;AAcA,WAAS,gBACP,SACA,cACA,KACA,YACA,kBACoB;AACpB,UAAM,MAAM,IAAI;AAChB,UAAM,WAAW,sBAAsB,SAAS,GAAG;AACnD,UAAM,cAAc,KAAK,KAAK,cAAc,GAAG,QAAQ,IAAI,GAAG,EAAE;AAEhE,QAAI,CAAC,WAAW,WAAW,GAAG;AAC5B,aAAO,EAAE,SAAS,aAAa,QAAQ,MAAM;AAAA,IAC/C;AAIA,QAAI,QAAQ,SAAS,SAAS,KAAK,kBAAkB;AACnD,YAAM,eAAeA,SAAQ,IAAI,IAAI;AACrC,YAAM,eAAe,iBAAiB,WAAW;AACjD,UAAI,iBAAiB,cAAc;AACjC,eAAO,EAAE,SAAS,aAAa,QAAQ,KAAK;AAAA,MAC9C;AAAA,IACF;AAEA,QAAI,IAAI;AACR,WAAO,MAAM;AACX,YAAM,YAAY,KAAK,KAAK,cAAc,GAAG,QAAQ,IAAI,CAAC,IAAI,GAAG,EAAE;AACnE,UAAI,CAAC,WAAW,SAAS,GAAG;AAC1B,eAAO,EAAE,SAAS,WAAW,QAAQ,MAAM;AAAA,MAC7C;AACA,WAAK;AAAA,IACP;AAAA,EACF;AAcA,WAAS,iBACP,cACA,cACA,eACA,WACQ;AACR,UAAM,SAAS,KAAK,QAAQ,YAAY;AACxC,QAAI,cAAc,wBAAwB,eAAe;AACvD,aAAO,MAAM,QAAQ,KAAK,SAAS,eAAe,YAAY,CAAC;AAAA,IACjE;AACA,WAAO,QAAQ,KAAK,SAAS,QAAQ,YAAY,CAAC;AAAA,EACpD;AAEA,WAAS,QAAQ,GAAmB;AAClC,WAAO,EAAE,MAAM,KAAK,GAAG,EAAE,KAAK,GAAG;AAAA,EACnC;AAOA,WAAS,kBACP,SACA,SACA,YACQ;AACR,UAAM,MAAM,KAAK,QAAQ,OAAO;AAChC,UAAM,MAAM,KAAK,QAAQ,OAAO;AAChC,UAAM,OAAO,KAAK,SAAS,SAAS,GAAG;AACvC,UAAM,OAAO,GAAG,IAAI,IAAI,KAAK,MAAM,OAAO,CAAC;AAC3C,UAAM,UAAU,KAAK,KAAK,KAAK,GAAG,IAAI,GAAG,GAAG,EAAE;AAC9C,QAAI,CAAC,WAAW,OAAO,GAAG;AAAC,aAAO;AAAA,IAAQ;AAC1C,QAAI,IAAI;AACR,WAAO,MAAM;AACX,YAAM,YAAY,KAAK,KAAK,KAAK,GAAG,IAAI,IAAI,CAAC,GAAG,GAAG,EAAE;AACrD,UAAI,CAAC,WAAW,SAAS,GAAG;AAAC,eAAO;AAAA,MAAU;AAC9C,WAAK;AAAA,IACP;AAAA,EACF;AAQA,WAAS,uBACP,MACA,aACA,eACU;AACV,UAAM,MAAgB,CAAC;AACvB,eAAW,OAAO,MAAM;AACtB,UAAI,KAAK,WAAW,GAAG,GAAG;AAGxB,YAAI,IAAI,WAAW,GAAG,KAAK,eAAe;AACxC,cAAI,KAAK,KAAK,KAAK,eAAe,GAAG,CAAC;AAAA,QACxC,OAAO;AACL,cAAI,KAAK,GAAG;AAAA,QACd;AACA;AAAA,MACF;AACA,UAAI,KAAK,KAAK,QAAQ,aAAa,GAAG,CAAC;AAAA,IACzC;AACA,WAAO;AAAA,EACT;AAOA,WAAS,sBACP,cACA,QACA,cACA,eACS;AACT,UAAM,SAAS,oBAAoB,QAAQ,cAAc,aAAa;AAGtE,UAAM,gBAAgB,OAAO,SAAS,KAAK,GAAG,IAAI,SAAS,SAAS,KAAK;AACzE,WAAO,iBAAiB,UAAU,aAAa,WAAW,aAAa;AAAA,EACzE;AASA,WAAS,aAAa,WAAmB,WAA4B;AACnE,UAAM,MAAM,KAAK,SAAS,WAAW,SAAS;AAC9C,WAAO,QAAQ,MAAO,CAAC,IAAI,WAAW,IAAI,KAAK,CAAC,KAAK,WAAW,GAAG;AAAA,EACrE;AAWA,WAAS,uBAAuB,SAAiB,OAA0B;AACzE,WAAO,CAAC,MAAM,KAAK,UAAQ,aAAa,MAAM,OAAO,CAAC;AAAA,EACxD;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;","names":["sha1Hex"]}
1
+ {"version":3,"sources":["../../src/shared/imagePaste.ts"],"sourcesContent":["// Image-paste helpers shared by the VS Code extension host and the desktop\n// app. Platform-neutral: no Node `path`/`crypto` imports. Path-dependent\n// helpers come from `createImagePasteHelpers(env)`, where the host injects its\n// path implementation (Node `path` in the extension) and optionally a SHA-1\n// function (defaults to the pure `sha1Hex` in `./sha1`).\nimport { sha1Hex as pureSha1Hex } from './sha1';\n\nexport interface ImagePasteSettings {\n enabled: boolean;\n folder: string;\n folderRelativeTo: 'document' | 'workspace';\n filenamePattern: string;\n pathStyle: 'relative' | 'workspace-absolute';\n altText: 'empty' | 'filename' | 'prompt';\n maxSizeMB: number;\n cleanupUnreferenced: boolean;\n}\n\n/** The subset of Node's `path` module these helpers use. */\nexport interface PathOps {\n sep: string;\n join(...parts: string[]): string;\n resolve(...parts: string[]): string;\n dirname(p: string): string;\n basename(p: string, ext?: string): string;\n extname(p: string): string;\n relative(from: string, to: string): string;\n isAbsolute(p: string): boolean;\n normalize(p: string): string;\n}\n\nexport interface ImagePasteEnv {\n path: PathOps;\n /** Full lowercase hex SHA-1 of the bytes. Defaults to the pure implementation. */\n sha1Hex?: (data: Uint8Array) => string;\n}\n\nconst MIME_TO_EXT: Record<string, string> = {\n 'image/png': 'png',\n 'image/jpeg': 'jpg',\n 'image/jpg': 'jpg',\n 'image/gif': 'gif',\n 'image/webp': 'webp',\n 'image/svg+xml': 'svg',\n 'image/bmp': 'bmp',\n 'image/x-icon': 'ico',\n 'image/tiff': 'tiff',\n 'image/avif': 'avif',\n};\n\nexport function extensionFromMime(mime: string): string | null {\n return MIME_TO_EXT[mime.toLowerCase()] ?? null;\n}\n\nexport interface FilenameContext {\n /** Markdown document base name without extension (e.g. \"my-note\"). */\n docName: string;\n /** Output extension without dot (e.g. \"png\"). */\n extension: string;\n /** Image bytes — used for the `${hash}` token. */\n data: Uint8Array;\n /** Timestamp used for `${date}`/`${time}`/`${timestamp}` tokens. */\n timestamp: Date;\n /** Auto-increment value for the `${index}` token. Defaults to 1. */\n index?: number;\n}\n\n/**\n * Strip characters that don't belong in a filename (Windows-strict set + control chars).\n * Collapses runs of whitespace/separators to a single dash.\n */\nexport function sanitizeForFilename(input: string): string {\n return input\n // eslint-disable-next-line no-control-regex -- intentional: strips ASCII control chars (\\x00-\\x1f) from filenames\n .replace(/[<>:\"/\\\\|?*\\x00-\\x1f]/g, '')\n .replace(/\\s+/g, '-')\n .replace(/-+/g, '-')\n .replace(/^[.-]+|[.-]+$/g, '');\n}\n\nexport interface FilenameResolution {\n /** Final absolute path to the image on disk. */\n absPath: string;\n /** True when an existing identical file was reused (hash-dedupe hit). */\n reused: boolean;\n}\n\n/**\n * Pull all `![alt](src)` matches out of a markdown string and return their\n * `src` values. Skips external (http/https) and data URIs since those don't\n * map to local files we'd ever want to delete.\n */\nexport function extractLocalImageRefs(markdown: string): string[] {\n const out: string[] = [];\n const re = /!\\[([^\\]]*)\\]\\(([^)]+)\\)/g;\n let m: RegExpExecArray | null;\n while ((m = re.exec(markdown)) !== null) {\n const src = m[2].trim();\n if (!src) {continue;}\n if (/^(?:[a-z][a-z0-9+.-]*:|data:|\\/\\/)/i.test(src) && !src.startsWith('/')) {continue;}\n // Strip cache-bust query/fragment that the resize popover may have left in.\n const queryStart = src.search(/[?#]/);\n out.push(queryStart >= 0 ? src.slice(0, queryStart) : src);\n }\n return out;\n}\n\n/**\n * Heuristic: does `filename` look like a MikeDown-generated paste filename\n * (rather than a user-curated asset like `logo.png`)?\n *\n * Builds a regex from the configured `filenamePattern` by replacing tokens\n * with shape-anchors (`${timestamp}` → `\\d{8}-\\d{6}`, `${hash}` → 8 hex chars,\n * etc.). Allows an optional `-N` collision suffix and any image extension.\n *\n * Returns false on any pattern parse failure so we never accidentally widen\n * the cleanup set when the user has set a weird custom pattern.\n */\nexport function looksLikeAutoPastedImage(\n filename: string,\n pattern: string,\n docName: string\n): boolean {\n if (!pattern) {return false;}\n const sanitizedDoc = sanitizeForFilename(docName) || 'image';\n const escapeRegex = (s: string): string => s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n\n let regex = '^';\n let i = 0;\n while (i < pattern.length) {\n const tail = pattern.slice(i);\n const tokenMatch = /^\\$\\{(\\w+)\\}/.exec(tail);\n if (tokenMatch) {\n const key = tokenMatch[1];\n switch (key) {\n case 'docName': regex += escapeRegex(sanitizedDoc); break;\n case 'date': regex += '\\\\d{8}'; break;\n case 'time': regex += '\\\\d{6}'; break;\n case 'timestamp': regex += '\\\\d{8}-\\\\d{6}'; break;\n case 'hash': regex += '[0-9a-f]{8}'; break;\n case 'index': regex += '\\\\d+'; break;\n // Unknown token — bail rather than match too broadly.\n default: return false;\n }\n i += tokenMatch[0].length;\n } else {\n regex += escapeRegex(pattern[i]);\n i += 1;\n }\n }\n regex += '(?:-\\\\d+)?\\\\.[a-zA-Z0-9]+$';\n try {\n return new RegExp(regex).test(filename);\n } catch {\n return false;\n }\n}\n\n/**\n * Decide what alt text to use, given the configured strategy and (optionally)\n * a user-provided value from the prompt UI.\n */\nexport function resolveAltText(\n strategy: 'empty' | 'filename' | 'prompt',\n filenameNoExt: string,\n promptedValue: string | undefined\n): string {\n if (strategy === 'filename') {return filenameNoExt;}\n if (strategy === 'prompt') {return promptedValue ?? '';}\n return '';\n}\n\n/**\n * Build the path- and hash-dependent helpers for one host. The extension\n * passes Node's `path` and a `crypto` SHA-1; the desktop app passes its own\n * synchronous path implementation (the pure SHA-1 default works there).\n */\nexport function createImagePasteHelpers(env: ImagePasteEnv) {\n const path = env.path;\n const hashImpl = env.sha1Hex ?? pureSha1Hex;\n\n /**\n * Replace pattern tokens with concrete values. Unknown tokens pass through\n * untouched so user-typed text stays intact.\n *\n * Supported tokens:\n * ${docName} sanitized document base name\n * ${date} YYYYMMDD\n * ${time} HHmmss\n * ${timestamp} YYYYMMDD-HHmmss\n * ${hash} 8-char SHA-1 of the image bytes\n * ${index} auto-increment value (default 1)\n */\n function expandFilenamePattern(pattern: string, ctx: FilenameContext): string {\n const pad = (n: number, w = 2): string => String(n).padStart(w, '0');\n const t = ctx.timestamp;\n const date = `${t.getFullYear()}${pad(t.getMonth() + 1)}${pad(t.getDate())}`;\n const time = `${pad(t.getHours())}${pad(t.getMinutes())}${pad(t.getSeconds())}`;\n const timestamp = `${date}-${time}`;\n\n // Lazy: only compute the hash if the pattern actually uses it.\n let hash = '';\n if (pattern.includes('${hash}')) {\n hash = sha1Hex(ctx.data).slice(0, 8);\n }\n\n const tokens: Record<string, string> = {\n docName: sanitizeForFilename(ctx.docName) || 'image',\n date,\n time,\n timestamp,\n hash,\n index: String(ctx.index ?? 1),\n };\n\n return pattern.replace(/\\$\\{(\\w+)\\}/g, (match: string, key: string) => {\n return Object.prototype.hasOwnProperty.call(tokens, key) ? tokens[key] : match;\n });\n }\n\n function sha1Hex(data: Uint8Array): string {\n return hashImpl(data);\n }\n\n /**\n * Resolve the absolute folder where pasted images should be written.\n * `workspaceRoot` may be undefined when the document is outside any workspace —\n * in that case `folderRelativeTo === 'workspace'` falls back to the document's\n * own directory.\n */\n function resolveTargetFolder(\n config: ImagePasteSettings,\n documentPath: string,\n workspaceRoot: string | undefined\n ): string {\n const folder = expandFolderPattern(config.folder, documentPath);\n if (path.isAbsolute(folder)) {\n return folder;\n }\n const base = config.folderRelativeTo === 'workspace' && workspaceRoot\n ? workspaceRoot\n : path.dirname(documentPath);\n return path.resolve(base, folder);\n }\n\n /**\n * Expand `${docName}` in the configured folder so each document can get its\n * own image folder (e.g. `assets/${docName}`). Only `${docName}` is supported:\n * time-based tokens would scatter one document's images across folders and\n * break orphan-cleanup scoping. Other tokens pass through untouched.\n */\n function expandFolderPattern(folder: string, documentPath: string): string {\n const docName = path.basename(documentPath, path.extname(documentPath));\n const safe = sanitizeForFilename(docName) || 'image';\n return folder.replace(/\\$\\{docName\\}/g, safe);\n }\n\n /**\n * Compute the final on-disk path, handling collisions and hash-dedupe.\n *\n * Behavior:\n * - If the resolved name (with extension) doesn't exist → use it verbatim.\n * - If it exists AND `${hash}` is in the pattern AND content hashes match\n * (per `readExistingHash`) → reuse the existing file (`reused: true`).\n * - Otherwise → append `-1`, `-2`, … until a free slot is found.\n *\n * `readExistingHash` is injected so this function stays pure / testable; pass\n * a real fs-backed implementation from the production caller.\n */\n function resolveFilename(\n pattern: string,\n targetFolder: string,\n ctx: FilenameContext,\n fileExists: (absPath: string) => boolean,\n readExistingHash?: (absPath: string) => string\n ): FilenameResolution {\n const ext = ctx.extension;\n const baseName = expandFilenamePattern(pattern, ctx);\n const initialPath = path.join(targetFolder, `${baseName}.${ext}`);\n\n if (!fileExists(initialPath)) {\n return { absPath: initialPath, reused: false };\n }\n\n // Hash-dedupe: only triggered when the pattern explicitly opts in via ${hash}.\n // Without that opt-in we always create a new file (collision suffix path).\n if (pattern.includes('${hash}') && readExistingHash) {\n const incomingHash = sha1Hex(ctx.data);\n const existingHash = readExistingHash(initialPath);\n if (existingHash === incomingHash) {\n return { absPath: initialPath, reused: true };\n }\n }\n\n let i = 1;\n while (true) {\n const candidate = path.join(targetFolder, `${baseName}-${i}.${ext}`);\n if (!fileExists(candidate)) {\n return { absPath: candidate, reused: false };\n }\n i += 1;\n }\n }\n\n /**\n * Produce the path string to write into the markdown image link.\n *\n * `relative` — POSIX-relative from the document's directory\n * (e.g. `images/foo.png`). Anchors with `./` only when\n * the path would otherwise look like a sibling file\n * starting with a letter — leaving plain `images/foo.png`\n * is the typical convention and what users expect.\n * `workspace-absolute` — leading `/`, POSIX-relative from the workspace root.\n * Falls back to the document-relative form when the\n * document is outside any workspace.\n */\n function formatInsertPath(\n absImagePath: string,\n documentPath: string,\n workspaceRoot: string | undefined,\n pathStyle: 'relative' | 'workspace-absolute'\n ): string {\n const docDir = path.dirname(documentPath);\n if (pathStyle === 'workspace-absolute' && workspaceRoot) {\n return '/' + toPosix(path.relative(workspaceRoot, absImagePath));\n }\n return toPosix(path.relative(docDir, absImagePath));\n }\n\n function toPosix(p: string): string {\n return p.split(path.sep).join('/');\n }\n\n /**\n * Compute a sibling filename for a resized variant (e.g. `foo.png` + 50 →\n * `foo-50pct.png`). If that sibling already exists, append `-1`, `-2`, …\n * until a free slot is found.\n */\n function resizeSiblingPath(\n absPath: string,\n percent: number,\n fileExists: (absPath: string) => boolean\n ): string {\n const dir = path.dirname(absPath);\n const ext = path.extname(absPath);\n const base = path.basename(absPath, ext);\n const stem = `${base}-${Math.round(percent)}pct`;\n const initial = path.join(dir, `${stem}${ext}`);\n if (!fileExists(initial)) {return initial;}\n let i = 1;\n while (true) {\n const candidate = path.join(dir, `${stem}-${i}${ext}`);\n if (!fileExists(candidate)) {return candidate;}\n i += 1;\n }\n }\n\n /**\n * Resolve a list of image-link srcs (as parsed from markdown) to absolute fs\n * paths. `documentDir` is the directory of the markdown file the srcs came\n * from. `workspaceRoot` is required to resolve workspace-absolute paths\n * (those starting with `/`).\n */\n function resolveLocalImagePaths(\n srcs: string[],\n documentDir: string,\n workspaceRoot: string | undefined\n ): string[] {\n const out: string[] = [];\n for (const src of srcs) {\n if (path.isAbsolute(src)) {\n // A leading-slash path is workspace-absolute when we have a workspace,\n // otherwise it's a real absolute path on the host filesystem.\n if (src.startsWith('/') && workspaceRoot) {\n out.push(path.join(workspaceRoot, src));\n } else {\n out.push(src);\n }\n continue;\n }\n out.push(path.resolve(documentDir, src));\n }\n return out;\n }\n\n /**\n * True iff `absImagePath` lives inside the resolved imagePaste folder for\n * this document. Used by orphan-cleanup to scope deletion to MikeDown-managed\n * locations (we never touch images the user keeps elsewhere).\n */\n function isInsideManagedFolder(\n absImagePath: string,\n config: ImagePasteSettings,\n documentPath: string,\n workspaceRoot: string | undefined\n ): boolean {\n const folder = resolveTargetFolder(config, documentPath, workspaceRoot);\n // Use trailing-separator match so `/foo/imagesX/y.png` doesn't count as\n // being inside `/foo/images`.\n const folderWithSep = folder.endsWith(path.sep) ? folder : folder + path.sep;\n return absImagePath === folder || absImagePath.startsWith(folderWithSep);\n }\n\n /**\n * True iff `childPath` is `parentDir` itself or lives somewhere underneath it.\n * Uses `path.relative` so this is correct across separators; a relative\n * result that starts with `..` (or that is itself absolute, which\n * `path.relative` returns for cross-drive paths on Windows) means `childPath`\n * is outside `parentDir`.\n */\n function isPathInside(parentDir: string, childPath: string): boolean {\n const rel = path.relative(parentDir, childPath);\n return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));\n }\n\n /**\n * True iff `absPath` is outside every directory in `roots`. Used by the image\n * file picker (M4) to decide whether a picked file needs to be copied into\n * the image-paste target folder to be servable — VS Code's webview\n * `localResourceRoots` only covers the extension's own directories, the\n * document's folder, and workspace folders, so a file picked from elsewhere\n * (e.g. the user's Desktop) won't render unless it's copied in. Files already\n * inside one of `roots` are referenced in place.\n */\n function isOutsideResourceRoots(absPath: string, roots: string[]): boolean {\n return !roots.some(root => isPathInside(root, absPath));\n }\n\n return {\n expandFilenamePattern,\n resolveTargetFolder,\n expandFolderPattern,\n resolveFilename,\n formatInsertPath,\n resizeSiblingPath,\n resolveLocalImagePaths,\n isInsideManagedFolder,\n isPathInside,\n isOutsideResourceRoots,\n extensionFromMime,\n sanitizeForFilename,\n extractLocalImageRefs,\n looksLikeAutoPastedImage,\n resolveAltText,\n };\n}\n\nexport type ImagePasteHelpers = ReturnType<typeof createImagePasteHelpers>;\n"],"mappings":"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","names":["sha1Hex"]}
@@ -1,60 +1,6 @@
1
- // src/shared/sha1.ts
2
- function rotl(x, n) {
3
- return x << n | x >>> 32 - n;
4
- }
5
- function sha1Hex(data) {
6
- const len = data.length;
7
- const padded = new Uint8Array((len + 8 >> 6) + 1 << 6);
8
- padded.set(data);
9
- padded[len] = 128;
10
- const view = new DataView(padded.buffer);
11
- view.setUint32(padded.length - 8, Math.floor(len / 536870912));
12
- view.setUint32(padded.length - 4, len * 8 >>> 0);
13
- let h0 = 1732584193;
14
- let h1 = 4023233417;
15
- let h2 = 2562383102;
16
- let h3 = 271733878;
17
- let h4 = 3285377520;
18
- const w = new Uint32Array(80);
19
- for (let off = 0; off < padded.length; off += 64) {
20
- for (let i = 0; i < 16; i++) {
21
- w[i] = view.getUint32(off + i * 4);
22
- }
23
- for (let i = 16; i < 80; i++) {
24
- w[i] = rotl(w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16], 1);
25
- }
26
- let a = h0, b = h1, c = h2, d = h3, e = h4;
27
- for (let i = 0; i < 80; i++) {
28
- let f;
29
- let k;
30
- if (i < 20) {
31
- f = b & c | ~b & d;
32
- k = 1518500249;
33
- } else if (i < 40) {
34
- f = b ^ c ^ d;
35
- k = 1859775393;
36
- } else if (i < 60) {
37
- f = b & c | b & d | c & d;
38
- k = 2400959708;
39
- } else {
40
- f = b ^ c ^ d;
41
- k = 3395469782;
42
- }
43
- const t = rotl(a, 5) + f + e + k + w[i] >>> 0;
44
- e = d;
45
- d = c;
46
- c = rotl(b, 30) >>> 0;
47
- b = a;
48
- a = t;
49
- }
50
- h0 = h0 + a >>> 0;
51
- h1 = h1 + b >>> 0;
52
- h2 = h2 + c >>> 0;
53
- h3 = h3 + d >>> 0;
54
- h4 = h4 + e >>> 0;
55
- }
56
- return [h0, h1, h2, h3, h4].map((h) => h.toString(16).padStart(8, "0")).join("");
57
- }
1
+ import {
2
+ sha1Hex
3
+ } from "../chunk-QWKT57TR.js";
58
4
  export {
59
5
  sha1Hex
60
6
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/shared/sha1.ts"],"sourcesContent":["// Pure, synchronous SHA-1 over bytes. No node:crypto, so it runs in the\n// webview, a Tauri webview, and Node alike. Used only for the image-paste\n// `${hash}` token and hash-dedupe (content fingerprinting, not security);\n// hosts with a native hash (Node `crypto`) can inject theirs instead.\n\nfunction rotl(x: number, n: number): number {\n return (x << n) | (x >>> (32 - n));\n}\n\nexport function sha1Hex(data: Uint8Array): string {\n const len = data.length;\n const padded = new Uint8Array((((len + 8) >> 6) + 1) << 6);\n padded.set(data);\n padded[len] = 0x80;\n const view = new DataView(padded.buffer);\n // 64-bit big-endian bit length.\n view.setUint32(padded.length - 8, Math.floor(len / 0x20000000));\n view.setUint32(padded.length - 4, (len * 8) >>> 0);\n\n let h0 = 0x67452301;\n let h1 = 0xefcdab89;\n let h2 = 0x98badcfe;\n let h3 = 0x10325476;\n let h4 = 0xc3d2e1f0;\n const w = new Uint32Array(80);\n\n for (let off = 0; off < padded.length; off += 64) {\n for (let i = 0; i < 16; i++) {w[i] = view.getUint32(off + i * 4);}\n for (let i = 16; i < 80; i++) {w[i] = rotl(w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16], 1);}\n let a = h0, b = h1, c = h2, d = h3, e = h4;\n for (let i = 0; i < 80; i++) {\n let f: number;\n let k: number;\n if (i < 20) { f = (b & c) | (~b & d); k = 0x5a827999; }\n else if (i < 40) { f = b ^ c ^ d; k = 0x6ed9eba1; }\n else if (i < 60) { f = (b & c) | (b & d) | (c & d); k = 0x8f1bbcdc; }\n else { f = b ^ c ^ d; k = 0xca62c1d6; }\n const t = (rotl(a, 5) + f + e + k + w[i]) >>> 0;\n e = d; d = c; c = rotl(b, 30) >>> 0; b = a; a = t;\n }\n h0 = (h0 + a) >>> 0;\n h1 = (h1 + b) >>> 0;\n h2 = (h2 + c) >>> 0;\n h3 = (h3 + d) >>> 0;\n h4 = (h4 + e) >>> 0;\n }\n return [h0, h1, h2, h3, h4].map(h => h.toString(16).padStart(8, '0')).join('');\n}\n"],"mappings":";AAKA,SAAS,KAAK,GAAW,GAAmB;AAC1C,SAAQ,KAAK,IAAM,MAAO,KAAK;AACjC;AAEO,SAAS,QAAQ,MAA0B;AAChD,QAAM,MAAM,KAAK;AACjB,QAAM,SAAS,IAAI,YAAc,MAAM,KAAM,KAAK,KAAM,CAAC;AACzD,SAAO,IAAI,IAAI;AACf,SAAO,GAAG,IAAI;AACd,QAAM,OAAO,IAAI,SAAS,OAAO,MAAM;AAEvC,OAAK,UAAU,OAAO,SAAS,GAAG,KAAK,MAAM,MAAM,SAAU,CAAC;AAC9D,OAAK,UAAU,OAAO,SAAS,GAAI,MAAM,MAAO,CAAC;AAEjD,MAAI,KAAK;AACT,MAAI,KAAK;AACT,MAAI,KAAK;AACT,MAAI,KAAK;AACT,MAAI,KAAK;AACT,QAAM,IAAI,IAAI,YAAY,EAAE;AAE5B,WAAS,MAAM,GAAG,MAAM,OAAO,QAAQ,OAAO,IAAI;AAChD,aAAS,IAAI,GAAG,IAAI,IAAI,KAAK;AAAC,QAAE,CAAC,IAAI,KAAK,UAAU,MAAM,IAAI,CAAC;AAAA,IAAE;AACjE,aAAS,IAAI,IAAI,IAAI,IAAI,KAAK;AAAC,QAAE,CAAC,IAAI,KAAK,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,CAAC;AAAA,IAAE;AAC3F,QAAI,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI;AACxC,aAAS,IAAI,GAAG,IAAI,IAAI,KAAK;AAC3B,UAAI;AACJ,UAAI;AACJ,UAAI,IAAI,IAAI;AAAE,YAAK,IAAI,IAAM,CAAC,IAAI;AAAI,YAAI;AAAA,MAAY,WAC7C,IAAI,IAAI;AAAE,YAAI,IAAI,IAAI;AAAG,YAAI;AAAA,MAAY,WACzC,IAAI,IAAI;AAAE,YAAK,IAAI,IAAM,IAAI,IAAM,IAAI;AAAI,YAAI;AAAA,MAAY,OAC/D;AAAE,YAAI,IAAI,IAAI;AAAG,YAAI;AAAA,MAAY;AACtC,YAAM,IAAK,KAAK,GAAG,CAAC,IAAI,IAAI,IAAI,IAAI,EAAE,CAAC,MAAO;AAC9C,UAAI;AAAG,UAAI;AAAG,UAAI,KAAK,GAAG,EAAE,MAAM;AAAG,UAAI;AAAG,UAAI;AAAA,IAClD;AACA,SAAM,KAAK,MAAO;AAClB,SAAM,KAAK,MAAO;AAClB,SAAM,KAAK,MAAO;AAClB,SAAM,KAAK,MAAO;AAClB,SAAM,KAAK,MAAO;AAAA,EACpB;AACA,SAAO,CAAC,IAAI,IAAI,IAAI,IAAI,EAAE,EAAE,IAAI,OAAK,EAAE,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG,CAAC,EAAE,KAAK,EAAE;AAC/E;","names":[]}
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -1,15 +1,7 @@
1
- // src/shared/wordCount.ts
2
- function countWords(text) {
3
- const stripped = text.replace(/^#+\s+/gm, "").replace(/\*\*|__|\*|_|~~|`/g, "").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/!\[[^\]]*\]\([^)]+\)/g, "").replace(/```[\s\S]*?```/g, "").replace(/`[^`]+`/g, "").replace(/^[-*+]\s+/gm, "").replace(/^\d+\.\s+/gm, "").replace(/^>\s+/gm, "").replace(/^---+$/gm, "").trim();
4
- if (!stripped) {
5
- return 0;
6
- }
7
- return stripped.split(/\s+/).filter((w) => w.length > 0).length;
8
- }
9
- var WPM = 225;
10
- function readingMinutes(wordCount) {
11
- return Math.max(1, Math.ceil(wordCount / WPM));
12
- }
1
+ import {
2
+ countWords,
3
+ readingMinutes
4
+ } from "../chunk-GWCRPPCX.js";
13
5
  export {
14
6
  countWords,
15
7
  readingMinutes
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/shared/wordCount.ts"],"sourcesContent":["// Shared word-count + reading-time helpers. Used by the host status bar\n// (src/statusBar.ts) and the in-editor sidebar footer (src/webview/outlineSidebar.ts)\n// so both surfaces show the same numbers without duplicating the regex set.\n\n/**\n * Count \"words\" in markdown-flavored text after stripping syntax that should\n * not contribute to word count (markers, code fences, list bullets, etc.).\n *\n * Conservative on purpose — alt text inside links is kept (it's user-visible\n * prose), but fenced code blocks are dropped entirely.\n */\nexport function countWords(text: string): number {\n const stripped = text\n .replace(/^#+\\s+/gm, '')\n .replace(/\\*\\*|__|\\*|_|~~|`/g, '')\n .replace(/\\[([^\\]]+)\\]\\([^)]+\\)/g, '$1')\n .replace(/!\\[[^\\]]*\\]\\([^)]+\\)/g, '')\n .replace(/```[\\s\\S]*?```/g, '')\n .replace(/`[^`]+`/g, '')\n .replace(/^[-*+]\\s+/gm, '')\n .replace(/^\\d+\\.\\s+/gm, '')\n .replace(/^>\\s+/gm, '')\n .replace(/^---+$/gm, '')\n .trim();\n\n if (!stripped) {return 0;}\n return stripped.split(/\\s+/).filter(w => w.length > 0).length;\n}\n\nconst WPM = 225;\n\n/** Minutes (rounded up, minimum 1) it would take to read `wordCount` words. */\nexport function readingMinutes(wordCount: number): number {\n return Math.max(1, Math.ceil(wordCount / WPM));\n}\n"],"mappings":";AAWO,SAAS,WAAW,MAAsB;AAC/C,QAAM,WAAW,KACd,QAAQ,YAAY,EAAE,EACtB,QAAQ,sBAAsB,EAAE,EAChC,QAAQ,0BAA0B,IAAI,EACtC,QAAQ,yBAAyB,EAAE,EACnC,QAAQ,mBAAmB,EAAE,EAC7B,QAAQ,YAAY,EAAE,EACtB,QAAQ,eAAe,EAAE,EACzB,QAAQ,eAAe,EAAE,EACzB,QAAQ,WAAW,EAAE,EACrB,QAAQ,YAAY,EAAE,EACtB,KAAK;AAER,MAAI,CAAC,UAAU;AAAC,WAAO;AAAA,EAAE;AACzB,SAAO,SAAS,MAAM,KAAK,EAAE,OAAO,OAAK,EAAE,SAAS,CAAC,EAAE;AACzD;AAEA,IAAM,MAAM;AAGL,SAAS,eAAe,WAA2B;AACxD,SAAO,KAAK,IAAI,GAAG,KAAK,KAAK,YAAY,GAAG,CAAC;AAC/C;","names":[]}
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}