drafter-mcp 0.25.0 → 0.26.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/drafter-mcp.js +469 -99
- package/package.json +1 -1
package/drafter-mcp.js
CHANGED
|
@@ -23233,7 +23233,7 @@ function toError(value) {
|
|
|
23233
23233
|
return value instanceof Error ? value : new Error(String(value));
|
|
23234
23234
|
}
|
|
23235
23235
|
// package.json
|
|
23236
|
-
var version = "0.
|
|
23236
|
+
var version = "0.26.0";
|
|
23237
23237
|
// ../fountain/src/inline.ts
|
|
23238
23238
|
const MARKERS$1 = /[*_\\]/;
|
|
23239
23239
|
const isSpace$10 = (code) => code === void 0 || /\s/.test(String.fromCharCode(code));
|
|
@@ -25892,7 +25892,7 @@ function columns(s) {
|
|
|
25892
25892
|
return n;
|
|
25893
25893
|
}
|
|
25894
25894
|
const joined = (runs) => runs.map((run) => run.text).join("");
|
|
25895
|
-
const isBlank$
|
|
25895
|
+
const isBlank$2 = (line) => joined(line.runs).trim() === "" && (!line.right || joined(line.right.runs).trim() === "");
|
|
25896
25896
|
function colFor(frame, length) {
|
|
25897
25897
|
if (frame.align === "center") return frame.indent + Math.floor((frame.width - length) / 2);
|
|
25898
25898
|
if (frame.align === "right") return frame.indent + frame.width - length;
|
|
@@ -26072,7 +26072,7 @@ function titlePage(tp, template, height, diagnostics) {
|
|
|
26072
26072
|
page: {
|
|
26073
26073
|
number: 0,
|
|
26074
26074
|
lines: rows.map((row, k) => {
|
|
26075
|
-
if (row && !isBlank$
|
|
26075
|
+
if (row && !isBlank$2(row)) last = k;
|
|
26076
26076
|
return row ?? blankLine(tp.span.from);
|
|
26077
26077
|
}),
|
|
26078
26078
|
sceneNumbersLeft: [],
|
|
@@ -26293,7 +26293,7 @@ var Items = class {
|
|
|
26293
26293
|
lines,
|
|
26294
26294
|
spaceBefore: p.style.spaceBefore,
|
|
26295
26295
|
spaceAfter: p.style.spaceAfter ?? 0,
|
|
26296
|
-
keepWithNext: p.style.keepWithNext === true || lines.every(isBlank$
|
|
26296
|
+
keepWithNext: p.style.keepWithNext === true || lines.every(isBlank$2),
|
|
26297
26297
|
keepTogether: p.style.keepTogether === true
|
|
26298
26298
|
};
|
|
26299
26299
|
}
|
|
@@ -26324,7 +26324,7 @@ var Items = class {
|
|
|
26324
26324
|
if (!p) continue;
|
|
26325
26325
|
const kind = this.#blocks[m].kind;
|
|
26326
26326
|
for (const line of this.#lines(m, p)) {
|
|
26327
|
-
const tag = isBlank$
|
|
26327
|
+
const tag = isBlank$2(line) ? "gap" : kind === "parenthetical" ? "paren" : "text";
|
|
26328
26328
|
body.push(line);
|
|
26329
26329
|
tags.push(tag);
|
|
26330
26330
|
spoken.push(spoken.at(-1) + (tag === "text" ? 1 : 0));
|
|
@@ -26453,7 +26453,7 @@ var Filler = class {
|
|
|
26453
26453
|
*/
|
|
26454
26454
|
#canStart(item, room, space) {
|
|
26455
26455
|
if (item.type === "block") {
|
|
26456
|
-
const lead = Math.max(0, item.lines.findIndex((line) => !isBlank$
|
|
26456
|
+
const lead = Math.max(0, item.lines.findIndex((line) => !isBlank$2(line)));
|
|
26457
26457
|
return space + (item.keepTogether ? item.lines.length : Math.min(item.lines.length, lead + this.template.keepWithNextLines)) <= room;
|
|
26458
26458
|
}
|
|
26459
26459
|
if (item.type === "dual") return space + item.rows.length <= room;
|
|
@@ -26593,7 +26593,7 @@ var Pager = class {
|
|
|
26593
26593
|
/** Adds a line, starting a new page when this one is full. A page never starts blank. */
|
|
26594
26594
|
put(line) {
|
|
26595
26595
|
if (this.#lines.length >= this.height) this.newPage();
|
|
26596
|
-
const blank = isBlank$
|
|
26596
|
+
const blank = isBlank$2(line);
|
|
26597
26597
|
if (blank && this.#lines.length === 0) return;
|
|
26598
26598
|
const at = {
|
|
26599
26599
|
page: this.pages.length + 1,
|
|
@@ -30174,12 +30174,44 @@ async function imported(run) {
|
|
|
30174
30174
|
return got;
|
|
30175
30175
|
}
|
|
30176
30176
|
/**
|
|
30177
|
+
* The most paragraphs (text between blank lines) a file may have to be read: a long feature
|
|
30178
|
+
* script has a few thousand. Past this, a file's parse can outgrow a reader's heap
|
|
30179
|
+
* (READER_HEAP_MB), and in list_scripts or search_scripts the reader stopping would fail the whole
|
|
30180
|
+
* folder; so the file is refused alone, as too long (red/blue audit, 2026-11-03: a 3 MB file of
|
|
30181
|
+
* 540,000 one-line scenes and actions stopped every listing, where 360,000 still listed).
|
|
30182
|
+
*/
|
|
30183
|
+
const MAX_PARAGRAPHS = 3e5;
|
|
30184
|
+
/** Whether `text` has more than `max` paragraphs (lines ending in LF, CR, or CRLF), in one pass. */
|
|
30185
|
+
function moreParagraphsThan(text, max) {
|
|
30186
|
+
let paragraphs = 0;
|
|
30187
|
+
let blank = true;
|
|
30188
|
+
let lineBlank = true;
|
|
30189
|
+
for (let i = 0; i < text.length; i++) {
|
|
30190
|
+
const code = text.charCodeAt(i);
|
|
30191
|
+
if (code === 10 || code === 13) {
|
|
30192
|
+
if (code === 13 && text.charCodeAt(i + 1) === 10) i++;
|
|
30193
|
+
blank = lineBlank;
|
|
30194
|
+
lineBlank = true;
|
|
30195
|
+
} else if (code !== 32 && code !== 9 && lineBlank) {
|
|
30196
|
+
lineBlank = false;
|
|
30197
|
+
if (blank && ++paragraphs > max) return true;
|
|
30198
|
+
blank = false;
|
|
30199
|
+
}
|
|
30200
|
+
}
|
|
30201
|
+
return false;
|
|
30202
|
+
}
|
|
30203
|
+
/**
|
|
30177
30204
|
* The script inside `file`, named `name` (its shown name, which is all a refusal says), judged in
|
|
30178
30205
|
* Open's order. Throws NotRead when its bytes couldn't be read, NotOpened in Open's words for a
|
|
30179
30206
|
* file Open turns away, and Refused (the reader's own phrase) for text Open won't read: over 5 MB,
|
|
30180
|
-
* not UTF-8, more than text.
|
|
30207
|
+
* not UTF-8, more than text, or more than MAX_PARAGRAPHS paragraphs.
|
|
30181
30208
|
*/
|
|
30182
30209
|
async function readInside(file, name) {
|
|
30210
|
+
const inside = await readWhole(file, name);
|
|
30211
|
+
if (moreParagraphsThan(inside.text, 3e5)) throw new Refused(`it has more than ${MAX_PARAGRAPHS.toLocaleString("en-US")} paragraphs`);
|
|
30212
|
+
return inside;
|
|
30213
|
+
}
|
|
30214
|
+
async function readWhole(file, name) {
|
|
30183
30215
|
const kind = importKindOf(name);
|
|
30184
30216
|
const read = {
|
|
30185
30217
|
noFollow: true,
|
|
@@ -30894,12 +30926,13 @@ function signatureLines(candidates, used = () => false) {
|
|
|
30894
30926
|
}
|
|
30895
30927
|
// ../story-map/src/mailto.ts
|
|
30896
30928
|
/** What a contact may never hold: spaces, controls, and anything that ends or reshapes a URL. */
|
|
30897
|
-
const REFUSED = /[\s?&"
|
|
30929
|
+
const REFUSED = /[\s?&"<>:\\/,;%\u0000-\u001f\u007f]/;
|
|
30898
30930
|
/**
|
|
30899
30931
|
* A `mailto:` link for a contact the page may link (design §10.8): one `@`, neither first nor
|
|
30900
30932
|
* last; a dot in the part after it, with something on both sides of it; and no space, control
|
|
30901
|
-
* character, `?` or `&` (which would add headers or a body to the mail),
|
|
30902
|
-
*
|
|
30933
|
+
* character, `?` or `&` (which would add headers or a body to the mail), comma, semicolon, or
|
|
30934
|
+
* `%` (which would add another address, plainly or encoded: red/blue audit, 2026-11-03), quote,
|
|
30935
|
+
* angle bracket, colon, or slash (which could end the attribute or make it another kind of link). Else null, and
|
|
30903
30936
|
* the page shows the contact as text.
|
|
30904
30937
|
*/
|
|
30905
30938
|
function mailtoOf(contact) {
|
|
@@ -30916,18 +30949,18 @@ const MOST_HOOK = 200;
|
|
|
30916
30949
|
/** The planned length's bounds, in pages. */
|
|
30917
30950
|
const FEWEST_PAGES = 1;
|
|
30918
30951
|
const MOST_PAGES = 999;
|
|
30919
|
-
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
30952
|
+
const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
30920
30953
|
/** The `storyMap` part of a parsed metadata block (`doc.meta`), read strictly. */
|
|
30921
30954
|
function storyMapMeta(meta) {
|
|
30922
30955
|
const hooks = Object.create(null);
|
|
30923
|
-
const part = isRecord(meta) ? meta.storyMap : void 0;
|
|
30924
|
-
if (!isRecord(part)) return {
|
|
30956
|
+
const part = isRecord$1(meta) ? meta.storyMap : void 0;
|
|
30957
|
+
if (!isRecord$1(part)) return {
|
|
30925
30958
|
plannedPages: null,
|
|
30926
30959
|
hooks
|
|
30927
30960
|
};
|
|
30928
30961
|
const pages = part.plannedPages;
|
|
30929
30962
|
const plannedPages = typeof pages === "number" && Number.isInteger(pages) && pages >= FEWEST_PAGES && pages <= MOST_PAGES ? pages : null;
|
|
30930
|
-
if (isRecord(part.hooks)) {
|
|
30963
|
+
if (isRecord$1(part.hooks)) {
|
|
30931
30964
|
for (const [key, hook] of Object.entries(part.hooks)) if (typeof hook === "string" && hook.length <= MOST_HOOK) hooks[key] = hook;
|
|
30932
30965
|
}
|
|
30933
30966
|
return {
|
|
@@ -31492,6 +31525,66 @@ function shares(amounts) {
|
|
|
31492
31525
|
}
|
|
31493
31526
|
return whole;
|
|
31494
31527
|
}
|
|
31528
|
+
// ../markdown/src/meta.ts
|
|
31529
|
+
const OPENER = "<!-- drafter ";
|
|
31530
|
+
const CLOSER = " -->";
|
|
31531
|
+
function isSpace$6(code) {
|
|
31532
|
+
return code === 32 || code === 9 || code === 10 || code === 13;
|
|
31533
|
+
}
|
|
31534
|
+
/**
|
|
31535
|
+
* The document's metadata block: its last line (trailing spaces and blank lines aside), starting
|
|
31536
|
+
* at the line's start (so not indented code) as `<!-- drafter `, ending ` -->`, and holding a JSON
|
|
31537
|
+
* object. Anything else, a comment with words after it, or one whose JSON doesn't read as an
|
|
31538
|
+
* object, is the writer's own text. One backwards scan, then one JSON parse: linear.
|
|
31539
|
+
*/
|
|
31540
|
+
function findMarkdownMeta(text) {
|
|
31541
|
+
let end = text.length;
|
|
31542
|
+
while (end > 0 && isSpace$6(text.charCodeAt(end - 1))) end--;
|
|
31543
|
+
const from = text.lastIndexOf("\n", end - 1) + 1;
|
|
31544
|
+
if (end - from < 17) return null;
|
|
31545
|
+
if (!text.startsWith(OPENER, from) || !text.startsWith(CLOSER, end - 4)) return null;
|
|
31546
|
+
let meta;
|
|
31547
|
+
try {
|
|
31548
|
+
meta = JSON.parse(text.slice(from + 13, end - 4));
|
|
31549
|
+
} catch {
|
|
31550
|
+
return null;
|
|
31551
|
+
}
|
|
31552
|
+
if (meta === null || typeof meta !== "object" || Array.isArray(meta)) return null;
|
|
31553
|
+
return {
|
|
31554
|
+
from,
|
|
31555
|
+
to: end,
|
|
31556
|
+
meta
|
|
31557
|
+
};
|
|
31558
|
+
}
|
|
31559
|
+
/**
|
|
31560
|
+
* The document's text and its metadata. The body is the text without the block, the line break
|
|
31561
|
+
* after it, and the one blank line before it that `setMarkdownMeta` wrote, so a body written with
|
|
31562
|
+
* `setMarkdownMeta` comes back exactly. No block: the whole text, and `meta: null`.
|
|
31563
|
+
*/
|
|
31564
|
+
function readMarkdownMeta(text) {
|
|
31565
|
+
const block = findMarkdownMeta(text);
|
|
31566
|
+
if (!block) return {
|
|
31567
|
+
body: text,
|
|
31568
|
+
meta: null
|
|
31569
|
+
};
|
|
31570
|
+
const before = text.slice(0, block.from);
|
|
31571
|
+
const nl = text.startsWith("\r\n", block.to) ? "\r\n" : text.startsWith("\n", block.to) ? "\n" : before.endsWith("\r\n\r\n") ? "\r\n" : "\n";
|
|
31572
|
+
const gap = nl + nl;
|
|
31573
|
+
return {
|
|
31574
|
+
body: before.endsWith(gap) ? before.slice(0, -gap.length) : before,
|
|
31575
|
+
meta: block.meta
|
|
31576
|
+
};
|
|
31577
|
+
}
|
|
31578
|
+
/**
|
|
31579
|
+
* The document as prose: its body (`readMarkdownMeta`), the text without its trailing metadata
|
|
31580
|
+
* block, the line breaks after it, and the blank line `setMarkdownMeta` wrote before it. The block
|
|
31581
|
+
* is Drafter's, not the writer's: the Preview, Print, and every export draw the prose, the word
|
|
31582
|
+
* counts count it, and the cut finder and Compare compare it. The prose is the text's start, so an
|
|
31583
|
+
* offset into it is the same offset into the text. No block: the whole text.
|
|
31584
|
+
*/
|
|
31585
|
+
function markdownProse(text) {
|
|
31586
|
+
return readMarkdownMeta(text).body;
|
|
31587
|
+
}
|
|
31495
31588
|
// ../markdown/src/words.ts
|
|
31496
31589
|
/**
|
|
31497
31590
|
* Whether a UTF-16 code unit is one that `/\s/` matches: the ASCII spaces (tab, line feed,
|
|
@@ -31500,7 +31593,7 @@ function shares(amounts) {
|
|
|
31500
31593
|
* medium mathematical space, the ideographic space, and the byte-order mark. A comparison per
|
|
31501
31594
|
* character, where a regular expression per character cost more than the reparse on a long script.
|
|
31502
31595
|
*/
|
|
31503
|
-
function isSpace$
|
|
31596
|
+
function isSpace$5(code) {
|
|
31504
31597
|
if (code <= 32) return code === 32 || code >= 9 && code <= 13;
|
|
31505
31598
|
if (code < 160) return false;
|
|
31506
31599
|
return code === 160 || code === 5760 || code >= 8192 && code <= 8202 || code === 8232 || code === 8233 || code === 8239 || code === 8287 || code === 12288 || code === 65279;
|
|
@@ -31510,7 +31603,7 @@ function wordsIn(text) {
|
|
|
31510
31603
|
let count = 0;
|
|
31511
31604
|
let inWord = false;
|
|
31512
31605
|
for (let i = 0; i < text.length; i++) {
|
|
31513
|
-
const space = isSpace$
|
|
31606
|
+
const space = isSpace$5(text.charCodeAt(i));
|
|
31514
31607
|
if (!space && !inWord) count++;
|
|
31515
31608
|
inWord = !space;
|
|
31516
31609
|
}
|
|
@@ -31547,7 +31640,7 @@ const SMALL = /* @__PURE__ */ new Set([
|
|
|
31547
31640
|
const cased = (c) => c.toLowerCase() !== c.toUpperCase();
|
|
31548
31641
|
const isUpper$1 = (c) => cased(c) && c === c.toUpperCase();
|
|
31549
31642
|
const isLower = (c) => cased(c) && c === c.toLowerCase();
|
|
31550
|
-
const isSpace$
|
|
31643
|
+
const isSpace$4 = (c) => c !== void 0 && c !== "" && c.trim() === "";
|
|
31551
31644
|
/** One word in title case: its first letter, and the first after a hyphen, in capitals. */
|
|
31552
31645
|
function titleWord(word, first) {
|
|
31553
31646
|
const chars = Array.from(word);
|
|
@@ -31570,7 +31663,7 @@ function titleWord(word, first) {
|
|
|
31570
31663
|
function titleCase(text) {
|
|
31571
31664
|
let first = true;
|
|
31572
31665
|
return text.split(/(\s+)/).map((part) => {
|
|
31573
|
-
if (part === "" || isSpace$
|
|
31666
|
+
if (part === "" || isSpace$4(part[0])) return part;
|
|
31574
31667
|
const word = titleWord(part, first);
|
|
31575
31668
|
first = false;
|
|
31576
31669
|
return word;
|
|
@@ -31598,7 +31691,7 @@ function withoutNumber(text) {
|
|
|
31598
31691
|
function slashParts(part) {
|
|
31599
31692
|
const out = [];
|
|
31600
31693
|
let from = 0;
|
|
31601
|
-
for (let i = 0; i < part.length; i++) if (part[i] === "/" && (isSpace$
|
|
31694
|
+
for (let i = 0; i < part.length; i++) if (part[i] === "/" && (isSpace$4(part[i - 1]) || isSpace$4(part[i + 1]))) {
|
|
31602
31695
|
out.push(part.slice(from, i));
|
|
31603
31696
|
from = i + 1;
|
|
31604
31697
|
}
|
|
@@ -31614,7 +31707,7 @@ function placeWords(location) {
|
|
|
31614
31707
|
const text = withoutNumber(location);
|
|
31615
31708
|
const parts = [];
|
|
31616
31709
|
let from = 0;
|
|
31617
|
-
for (let i = 1; i < text.length - 1; i++) if (DASHES$1.has(text[i]) && isSpace$
|
|
31710
|
+
for (let i = 1; i < text.length - 1; i++) if (DASHES$1.has(text[i]) && isSpace$4(text[i - 1]) && isSpace$4(text[i + 1])) {
|
|
31618
31711
|
parts.push(text.slice(from, i));
|
|
31619
31712
|
from = i + 1;
|
|
31620
31713
|
}
|
|
@@ -31629,7 +31722,7 @@ const followsOn = (key) => key.startsWith("LATER ") || key.endsWith(" LATER");
|
|
|
31629
31722
|
*/
|
|
31630
31723
|
function followOn(location) {
|
|
31631
31724
|
const text = location.trimEnd();
|
|
31632
|
-
for (let i = text.length - 2; i > 0; i--) if (DASHES$1.has(text[i]) && isSpace$
|
|
31725
|
+
for (let i = text.length - 2; i > 0; i--) if (DASHES$1.has(text[i]) && isSpace$4(text[i - 1]) && isSpace$4(text[i + 1])) {
|
|
31633
31726
|
const part = text.slice(i + 1).trim();
|
|
31634
31727
|
if (!followsOn(part.toUpperCase().split(/\s+/).join(" "))) break;
|
|
31635
31728
|
return {
|
|
@@ -31795,7 +31888,7 @@ const TRAILING = /* @__PURE__ */ new Set([
|
|
|
31795
31888
|
]);
|
|
31796
31889
|
/** A comma or dashes just after a name (or its age), and the spaces after them: anchored, linear. */
|
|
31797
31890
|
const LEAD = /^[,\-–—―][\s,\-–—―]*/u;
|
|
31798
|
-
const isSpace$
|
|
31891
|
+
const isSpace$3 = (c) => c !== void 0 && c.trim() === "";
|
|
31799
31892
|
const isLetter = (c) => c !== void 0 && /\p{L}/u.test(c);
|
|
31800
31893
|
const isStop = (c) => c === "." || c === "!" || c === "?";
|
|
31801
31894
|
/** Whether the period at `i` closes an abbreviation or an initial (`Dr.`, `J.`). */
|
|
@@ -31818,7 +31911,7 @@ function sentenceEnd(text, from = 0, limit = text.length) {
|
|
|
31818
31911
|
let j = i + 1;
|
|
31819
31912
|
while (j < text.length && isStop(text[j])) j++;
|
|
31820
31913
|
while (j < text.length && CLOSERS.has(text[j])) j++;
|
|
31821
|
-
if (j < text.length && !isSpace$
|
|
31914
|
+
if (j < text.length && !isSpace$3(text[j])) {
|
|
31822
31915
|
i = j - 1;
|
|
31823
31916
|
continue;
|
|
31824
31917
|
}
|
|
@@ -31943,7 +32036,7 @@ function nameCase(text, map, names, ages = true) {
|
|
|
31943
32036
|
at += 2;
|
|
31944
32037
|
}
|
|
31945
32038
|
if (!ages) {
|
|
31946
|
-
const age = ageAt(text, isSpace$
|
|
32039
|
+
const age = ageAt(text, isSpace$3(text[at]) ? at + 1 : at);
|
|
31947
32040
|
if (age) at = age.end;
|
|
31948
32041
|
}
|
|
31949
32042
|
}
|
|
@@ -31964,7 +32057,7 @@ function withoutMarks(text) {
|
|
|
31964
32057
|
return text.replace(SCREEN_MARKS, (mark, at) => {
|
|
31965
32058
|
if (mark.startsWith("(") || !mark.endsWith(".")) return GONE;
|
|
31966
32059
|
let next = at + mark.length;
|
|
31967
|
-
while (isSpace$
|
|
32060
|
+
while (isSpace$3(text[next])) next++;
|
|
31968
32061
|
const c = text[next];
|
|
31969
32062
|
return c === void 0 || c !== c.toLowerCase() && c === c.toUpperCase() ? STOP_KEPT : GONE;
|
|
31970
32063
|
}).replace(/\s+/g, " ").replace(/ ?\uE001(?=[,.;:!?])/g, "").replace(/\uE001/g, "").replace(/ ?\uE000/g, ".").replace(/\s+/g, " ").trim();
|
|
@@ -32022,7 +32115,7 @@ function pastCapitals(text, at) {
|
|
|
32022
32115
|
let i = at;
|
|
32023
32116
|
for (let n = 0; n < NAME_WORDS; n++) {
|
|
32024
32117
|
let j = i;
|
|
32025
|
-
while (isSpace$
|
|
32118
|
+
while (isSpace$3(text[j])) j++;
|
|
32026
32119
|
let k = j;
|
|
32027
32120
|
while (k < text.length && /[\p{L}\p{M}'’.-]/u.test(text[k])) k++;
|
|
32028
32121
|
const word = text.slice(j, k).replace(/\.$/, "");
|
|
@@ -32039,11 +32132,11 @@ function pastCapitals(text, at) {
|
|
|
32039
32132
|
*/
|
|
32040
32133
|
function introducedAt(text, at) {
|
|
32041
32134
|
let j = pastCapitals(text, at);
|
|
32042
|
-
while (isSpace$
|
|
32135
|
+
while (isSpace$3(text[j])) j++;
|
|
32043
32136
|
if (ageAt(text, j)) return true;
|
|
32044
32137
|
const aside = asideEnd(text, j);
|
|
32045
32138
|
if (aside >= 0) j = aside;
|
|
32046
|
-
while (isSpace$
|
|
32139
|
+
while (isSpace$3(text[j])) j++;
|
|
32047
32140
|
const c = text[j];
|
|
32048
32141
|
return c === "," || c === "-" || c !== void 0 && DASHES.has(c);
|
|
32049
32142
|
}
|
|
@@ -32054,7 +32147,7 @@ function introducedAt(text, at) {
|
|
|
32054
32147
|
function hookFrom(text, at, map, names) {
|
|
32055
32148
|
let i = pastCapitals(text, at);
|
|
32056
32149
|
let j = i;
|
|
32057
|
-
while (isSpace$
|
|
32150
|
+
while (isSpace$3(text[j])) j++;
|
|
32058
32151
|
const aged = ageAt(text, j);
|
|
32059
32152
|
const aside = aged ? aged.end : asideEnd(text, j);
|
|
32060
32153
|
if (aside >= 0) i = aside;
|
|
@@ -32065,7 +32158,7 @@ function hookFrom(text, at, map, names) {
|
|
|
32065
32158
|
rest = rest.slice(lead.length);
|
|
32066
32159
|
if (lead !== "") for (let k = 0; k < rest.length; k++) {
|
|
32067
32160
|
const c = rest[k];
|
|
32068
|
-
const hyphen = c === "-" && (rest[k + 1] === "-" || isSpace$
|
|
32161
|
+
const hyphen = c === "-" && (rest[k + 1] === "-" || isSpace$3(rest[k - 1]) && isSpace$3(rest[k + 1]));
|
|
32069
32162
|
if (c === "," || DASHES.has(c) || hyphen) {
|
|
32070
32163
|
rest = rest.slice(0, k);
|
|
32071
32164
|
break;
|
|
@@ -33388,7 +33481,7 @@ function groupRows(rows, oldAnchors, newAnchors) {
|
|
|
33388
33481
|
});
|
|
33389
33482
|
return groups;
|
|
33390
33483
|
}
|
|
33391
|
-
const FENCE$
|
|
33484
|
+
const FENCE$2 = /^ {0,3}(`{3,}|~{3,})/;
|
|
33392
33485
|
const HEADING = /^ {0,3}(#{1,3})(?:[ \t]+(.*?))?(?:[ \t]+#+)?[ \t]*$/;
|
|
33393
33486
|
/**
|
|
33394
33487
|
* The lines of a Markdown text that start a heading of level 1 to 3, with its text, skipping fenced
|
|
@@ -33398,7 +33491,7 @@ function markdownAnchors(text) {
|
|
|
33398
33491
|
const anchors = [];
|
|
33399
33492
|
let fence = null;
|
|
33400
33493
|
splitLines(text).lines.forEach((line, i) => {
|
|
33401
|
-
const open = FENCE$
|
|
33494
|
+
const open = FENCE$2.exec(line);
|
|
33402
33495
|
if (fence !== null) {
|
|
33403
33496
|
if (open && open[1][0] === fence[0] && open[1].length >= fence.length && !line.slice(open[0].length).trim()) fence = null;
|
|
33404
33497
|
return;
|
|
@@ -33417,39 +33510,8 @@ function markdownAnchors(text) {
|
|
|
33417
33510
|
});
|
|
33418
33511
|
return anchors;
|
|
33419
33512
|
}
|
|
33420
|
-
// ../markdown/src/meta.ts
|
|
33421
|
-
const OPENER = "<!-- drafter ";
|
|
33422
|
-
const CLOSER = " -->";
|
|
33423
|
-
function isSpace$3(code) {
|
|
33424
|
-
return code === 32 || code === 9 || code === 10 || code === 13;
|
|
33425
|
-
}
|
|
33426
|
-
/**
|
|
33427
|
-
* The document's metadata block: its last line (trailing spaces and blank lines aside), starting
|
|
33428
|
-
* at the line's start (so not indented code) as `<!-- drafter `, ending ` -->`, and holding a JSON
|
|
33429
|
-
* object. Anything else, a comment with words after it, or one whose JSON doesn't read as an
|
|
33430
|
-
* object, is the writer's own text. One backwards scan, then one JSON parse: linear.
|
|
33431
|
-
*/
|
|
33432
|
-
function findMarkdownMeta(text) {
|
|
33433
|
-
let end = text.length;
|
|
33434
|
-
while (end > 0 && isSpace$3(text.charCodeAt(end - 1))) end--;
|
|
33435
|
-
const from = text.lastIndexOf("\n", end - 1) + 1;
|
|
33436
|
-
if (end - from < 17) return null;
|
|
33437
|
-
if (!text.startsWith(OPENER, from) || !text.startsWith(CLOSER, end - 4)) return null;
|
|
33438
|
-
let meta;
|
|
33439
|
-
try {
|
|
33440
|
-
meta = JSON.parse(text.slice(from + 13, end - 4));
|
|
33441
|
-
} catch {
|
|
33442
|
-
return null;
|
|
33443
|
-
}
|
|
33444
|
-
if (meta === null || typeof meta !== "object" || Array.isArray(meta)) return null;
|
|
33445
|
-
return {
|
|
33446
|
-
from,
|
|
33447
|
-
to: end,
|
|
33448
|
-
meta
|
|
33449
|
-
};
|
|
33450
|
-
}
|
|
33451
33513
|
// ../markdown/src/title.ts
|
|
33452
|
-
const isBlank = (c) => c === " " || c === " ";
|
|
33514
|
+
const isBlank$1 = (c) => c === " " || c === " ";
|
|
33453
33515
|
/**
|
|
33454
33516
|
* An ATX heading's parts, read by hand: up to three spaces, 1 to 6 `#` (`prefix`), then a space or
|
|
33455
33517
|
* a tab or the line's end, and the text, without the blanks around it or a closing run of `#`s (one
|
|
@@ -33463,23 +33525,23 @@ function atx(line) {
|
|
|
33463
33525
|
if (hashes < 1 || hashes > 6) return void 0;
|
|
33464
33526
|
const prefix = line.slice(0, i + hashes);
|
|
33465
33527
|
i += hashes;
|
|
33466
|
-
if (i < line.length && !isBlank(line[i])) return void 0;
|
|
33528
|
+
if (i < line.length && !isBlank$1(line[i])) return void 0;
|
|
33467
33529
|
let start = i;
|
|
33468
|
-
while (start < line.length && isBlank(line[start])) start++;
|
|
33530
|
+
while (start < line.length && isBlank$1(line[start])) start++;
|
|
33469
33531
|
let end = line.length;
|
|
33470
|
-
while (end > start && isBlank(line[end - 1])) end--;
|
|
33532
|
+
while (end > start && isBlank$1(line[end - 1])) end--;
|
|
33471
33533
|
let close = end;
|
|
33472
33534
|
while (close > start && line[close - 1] === "#") close--;
|
|
33473
|
-
if (close < end && (close === start || isBlank(line[close - 1]))) {
|
|
33535
|
+
if (close < end && (close === start || isBlank$1(line[close - 1]))) {
|
|
33474
33536
|
end = close;
|
|
33475
|
-
while (end > start && isBlank(line[end - 1])) end--;
|
|
33537
|
+
while (end > start && isBlank$1(line[end - 1])) end--;
|
|
33476
33538
|
}
|
|
33477
33539
|
return {
|
|
33478
33540
|
prefix,
|
|
33479
33541
|
text: line.slice(start, end)
|
|
33480
33542
|
};
|
|
33481
33543
|
}
|
|
33482
|
-
const FENCE = /^ {0,3}(`{3,}|~{3,})/;
|
|
33544
|
+
const FENCE$1 = /^ {0,3}(`{3,}|~{3,})/;
|
|
33483
33545
|
const SETEXT = /^ {0,3}(?:=+|-+)[ \t]*$/;
|
|
33484
33546
|
/**
|
|
33485
33547
|
* The label of a footnote definition at a line's start, after any indentation, quote marks, and
|
|
@@ -33488,15 +33550,15 @@ const SETEXT = /^ {0,3}(?:=+|-+)[ \t]*$/;
|
|
|
33488
33550
|
function definitionLabel(line) {
|
|
33489
33551
|
let i = 0;
|
|
33490
33552
|
for (;;) {
|
|
33491
|
-
while (isBlank(line[i]) || line[i] === ">") i++;
|
|
33553
|
+
while (isBlank$1(line[i]) || line[i] === ">") i++;
|
|
33492
33554
|
const c = line[i];
|
|
33493
|
-
if ((c === "-" || c === "*" || c === "+") && isBlank(line[i + 1])) {
|
|
33555
|
+
if ((c === "-" || c === "*" || c === "+") && isBlank$1(line[i + 1])) {
|
|
33494
33556
|
i++;
|
|
33495
33557
|
continue;
|
|
33496
33558
|
}
|
|
33497
33559
|
let j = i;
|
|
33498
33560
|
while (j - i < 9 && (line[j] ?? "") >= "0" && (line[j] ?? "") <= "9") j++;
|
|
33499
|
-
if (j > i && (line[j] === "." || line[j] === ")") && isBlank(line[j + 1])) {
|
|
33561
|
+
if (j > i && (line[j] === "." || line[j] === ")") && isBlank$1(line[j + 1])) {
|
|
33500
33562
|
i = j + 1;
|
|
33501
33563
|
continue;
|
|
33502
33564
|
}
|
|
@@ -33519,7 +33581,7 @@ function definedLabels(source) {
|
|
|
33519
33581
|
if (end < 0) end = source.length;
|
|
33520
33582
|
const line = source.slice(start, end);
|
|
33521
33583
|
start = end + 1;
|
|
33522
|
-
const opens = FENCE.exec(line);
|
|
33584
|
+
const opens = FENCE$1.exec(line);
|
|
33523
33585
|
if (fence !== void 0) {
|
|
33524
33586
|
if (opens?.[1]?.startsWith(fence.charAt(0)) && opens[1].length >= fence.length) fence = void 0;
|
|
33525
33587
|
continue;
|
|
@@ -33636,7 +33698,7 @@ function squeeze(text) {
|
|
|
33636
33698
|
for (let i = 0; i <= text.length; i++) {
|
|
33637
33699
|
const c = text[i];
|
|
33638
33700
|
const mark = c === "*" || c === "_" || c === "`";
|
|
33639
|
-
const blank = c !== void 0 && !mark && isSpace$
|
|
33701
|
+
const blank = c !== void 0 && !mark && isSpace$5(text.charCodeAt(i));
|
|
33640
33702
|
if (c !== void 0 && !mark && !blank) {
|
|
33641
33703
|
if (from < 0) {
|
|
33642
33704
|
if (space && parts.length > 0) parts.push(" ");
|
|
@@ -33675,7 +33737,7 @@ function markdownTitle(source) {
|
|
|
33675
33737
|
if (end < 0) end = source.length;
|
|
33676
33738
|
const line = source.slice(start, end);
|
|
33677
33739
|
start = end + 1;
|
|
33678
|
-
const opens = FENCE.exec(line);
|
|
33740
|
+
const opens = FENCE$1.exec(line);
|
|
33679
33741
|
if (fence !== void 0) {
|
|
33680
33742
|
if (opens?.[1]?.startsWith(fence.charAt(0)) && opens[1].length >= fence.length) fence = void 0;
|
|
33681
33743
|
before = "";
|
|
@@ -33697,6 +33759,212 @@ function markdownTitle(source) {
|
|
|
33697
33759
|
before = line.trim() === "" || /^ {4}/.test(line) ? "" : line;
|
|
33698
33760
|
}
|
|
33699
33761
|
}
|
|
33762
|
+
// ../novel/src/structure.ts
|
|
33763
|
+
const RANK = {
|
|
33764
|
+
part: 0,
|
|
33765
|
+
chapter: 1,
|
|
33766
|
+
section: 2
|
|
33767
|
+
};
|
|
33768
|
+
const FENCE = /^ {0,3}(`{3,}|~{3,})/;
|
|
33769
|
+
const ATX = /^ {0,3}(#{1,6})(?:[ \t]([^\n]*))?$/;
|
|
33770
|
+
const BREAK = /^ {0,3}([-*_])(?:[ \t]*\1){2,}[ \t]*$/;
|
|
33771
|
+
const SETEXT_1 = /^ {0,3}=+[ \t]*$/;
|
|
33772
|
+
const SETEXT_2 = /^ {0,3}-+[ \t]*$/;
|
|
33773
|
+
const NOT_PARAGRAPH_START = /^(?: {4}|\t|<!--)/;
|
|
33774
|
+
const CONTAINER_START = /^ {0,3}(?:>|[-*+](?:[ \t]|$)|\d{1,9}[.)](?:[ \t]|$))/;
|
|
33775
|
+
const PART = /^part(?:\s|$)/i;
|
|
33776
|
+
function isBlank(char) {
|
|
33777
|
+
return char === " " || char === " " || char === "\r";
|
|
33778
|
+
}
|
|
33779
|
+
/** An ATX heading's text: trimmed, without a closing run of `#` (which a space or tab must precede). */
|
|
33780
|
+
function atxTitle(rest) {
|
|
33781
|
+
let a = 0;
|
|
33782
|
+
let b = rest.length;
|
|
33783
|
+
while (a < b && isBlank(rest[a])) a++;
|
|
33784
|
+
while (b > a && isBlank(rest[b - 1])) b--;
|
|
33785
|
+
let c = b;
|
|
33786
|
+
while (c > a && rest[c - 1] === "#") c--;
|
|
33787
|
+
if (c < b && (c === a || isBlank(rest[c - 1]))) {
|
|
33788
|
+
b = c;
|
|
33789
|
+
while (b > a && isBlank(rest[b - 1])) b--;
|
|
33790
|
+
}
|
|
33791
|
+
return rest.slice(a, b);
|
|
33792
|
+
}
|
|
33793
|
+
function slug(title) {
|
|
33794
|
+
return title.toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "-").replace(/^-+|-+$/g, "");
|
|
33795
|
+
}
|
|
33796
|
+
/** Reads a novel's structure in one pass. The metadata comment, and what it holds, is not read. */
|
|
33797
|
+
function novelStructure(text) {
|
|
33798
|
+
const end = findMarkdownMeta(text)?.from ?? text.length;
|
|
33799
|
+
const roots = [];
|
|
33800
|
+
const seen = /* @__PURE__ */ new Map();
|
|
33801
|
+
const stack = [];
|
|
33802
|
+
let firstHeading = -1;
|
|
33803
|
+
let fence = null;
|
|
33804
|
+
let paraStart = -1;
|
|
33805
|
+
let paraText = "";
|
|
33806
|
+
let container = false;
|
|
33807
|
+
const open = (wanted, title, from, to) => {
|
|
33808
|
+
if (firstHeading < 0) firstHeading = from;
|
|
33809
|
+
const top = stack.length > 0 ? stack[stack.length - 1].node.kind : null;
|
|
33810
|
+
const kind = wanted === "section" && (top === null || top === "part") ? "chapter" : wanted;
|
|
33811
|
+
while (stack.length > 0 && RANK[stack[stack.length - 1].node.kind] >= RANK[kind]) {
|
|
33812
|
+
const done = stack.pop();
|
|
33813
|
+
done.node.span.to = from;
|
|
33814
|
+
}
|
|
33815
|
+
const siblings = stack.length > 0 ? stack[stack.length - 1].node.children : roots;
|
|
33816
|
+
const key = `${kind}:${slug(title)}`;
|
|
33817
|
+
const n = seen.get(key) ?? 0;
|
|
33818
|
+
seen.set(key, n + 1);
|
|
33819
|
+
const node = {
|
|
33820
|
+
id: `${key}:${n}`,
|
|
33821
|
+
kind,
|
|
33822
|
+
title,
|
|
33823
|
+
heading: {
|
|
33824
|
+
from,
|
|
33825
|
+
to
|
|
33826
|
+
},
|
|
33827
|
+
span: {
|
|
33828
|
+
from,
|
|
33829
|
+
to: end
|
|
33830
|
+
},
|
|
33831
|
+
words: 0,
|
|
33832
|
+
ownWords: 0,
|
|
33833
|
+
children: []
|
|
33834
|
+
};
|
|
33835
|
+
siblings.push(node);
|
|
33836
|
+
stack.push({ node });
|
|
33837
|
+
};
|
|
33838
|
+
let pos = 0;
|
|
33839
|
+
while (pos < end) {
|
|
33840
|
+
let eol = text.indexOf("\n", pos);
|
|
33841
|
+
if (eol < 0 || eol > end) eol = end;
|
|
33842
|
+
const next = eol < end ? eol + 1 : end;
|
|
33843
|
+
let lineEnd = eol;
|
|
33844
|
+
if (lineEnd > pos && text.charCodeAt(lineEnd - 1) === 13) lineEnd--;
|
|
33845
|
+
const line = text.slice(pos, lineEnd);
|
|
33846
|
+
const fenceMatch = FENCE.exec(line);
|
|
33847
|
+
if (fence) {
|
|
33848
|
+
if (fenceMatch && fenceMatch[1][0] === fence.char && fenceMatch[1].length >= fence.size && line.trim() === fenceMatch[1]) fence = null;
|
|
33849
|
+
paraStart = -1;
|
|
33850
|
+
} else if (fenceMatch && !(fenceMatch[1][0] === "`" && line.includes("`", fenceMatch[0].length))) {
|
|
33851
|
+
fence = {
|
|
33852
|
+
char: fenceMatch[1][0],
|
|
33853
|
+
size: fenceMatch[1].length
|
|
33854
|
+
};
|
|
33855
|
+
paraStart = -1;
|
|
33856
|
+
} else if (line.trim() === "") {
|
|
33857
|
+
paraStart = -1;
|
|
33858
|
+
container = false;
|
|
33859
|
+
} else {
|
|
33860
|
+
const atx = ATX.exec(line);
|
|
33861
|
+
if (atx) {
|
|
33862
|
+
paraStart = -1;
|
|
33863
|
+
const level = atx[1].length;
|
|
33864
|
+
if (level <= 2) {
|
|
33865
|
+
const title = atxTitle(atx[2] ?? "");
|
|
33866
|
+
open(level === 2 ? "section" : PART.test(title) ? "part" : "chapter", title, pos, lineEnd);
|
|
33867
|
+
}
|
|
33868
|
+
} else if (paraStart >= 0 && SETEXT_1.test(line)) {
|
|
33869
|
+
open(PART.test(paraText) ? "part" : "chapter", paraText, paraStart, lineEnd);
|
|
33870
|
+
paraStart = -1;
|
|
33871
|
+
} else if (paraStart >= 0 && SETEXT_2.test(line)) {
|
|
33872
|
+
open("section", paraText, paraStart, lineEnd);
|
|
33873
|
+
paraStart = -1;
|
|
33874
|
+
} else if (BREAK.test(line)) {
|
|
33875
|
+
paraStart = -1;
|
|
33876
|
+
if (stack.length > 0) open("section", "", pos, lineEnd);
|
|
33877
|
+
} else if (CONTAINER_START.test(line)) {
|
|
33878
|
+
paraStart = -1;
|
|
33879
|
+
container = true;
|
|
33880
|
+
} else if (container) {} else if (paraStart >= 0) paraText += ` ${line.trim()}`;
|
|
33881
|
+
else if (!NOT_PARAGRAPH_START.test(line)) {
|
|
33882
|
+
paraStart = pos;
|
|
33883
|
+
paraText = line.trim();
|
|
33884
|
+
}
|
|
33885
|
+
}
|
|
33886
|
+
pos = next;
|
|
33887
|
+
}
|
|
33888
|
+
const count = (node) => {
|
|
33889
|
+
const own = node.children.length > 0 ? node.children[0].heading.from : node.span.to;
|
|
33890
|
+
node.ownWords = wordsIn(node.title) + wordsIn(text.slice(node.heading.to, own));
|
|
33891
|
+
let words = node.ownWords;
|
|
33892
|
+
for (const child of node.children) {
|
|
33893
|
+
count(child);
|
|
33894
|
+
words += child.words;
|
|
33895
|
+
}
|
|
33896
|
+
node.words = words;
|
|
33897
|
+
};
|
|
33898
|
+
let words = 0;
|
|
33899
|
+
for (const node of roots) {
|
|
33900
|
+
count(node);
|
|
33901
|
+
words += node.words;
|
|
33902
|
+
}
|
|
33903
|
+
const stop = firstHeading < 0 ? end : firstHeading;
|
|
33904
|
+
const preamble = stop > 0 ? {
|
|
33905
|
+
from: 0,
|
|
33906
|
+
to: stop
|
|
33907
|
+
} : null;
|
|
33908
|
+
if (preamble) words += wordsIn(text.slice(0, stop));
|
|
33909
|
+
return {
|
|
33910
|
+
preamble,
|
|
33911
|
+
parts: roots.some((n) => n.kind === "part"),
|
|
33912
|
+
nodes: roots,
|
|
33913
|
+
words
|
|
33914
|
+
};
|
|
33915
|
+
}
|
|
33916
|
+
// ../novel/src/facts.ts
|
|
33917
|
+
const STATUSES = [
|
|
33918
|
+
"todo",
|
|
33919
|
+
"draft",
|
|
33920
|
+
"revised",
|
|
33921
|
+
"done"
|
|
33922
|
+
];
|
|
33923
|
+
/** The longest synopsis kept, in UTF-16 code units: a longer one is cut there when read or written. */
|
|
33924
|
+
const MAX_SYNOPSIS = 2e3;
|
|
33925
|
+
const MAX_TARGET = 1e7;
|
|
33926
|
+
function isRecord(value) {
|
|
33927
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
33928
|
+
}
|
|
33929
|
+
function target(value) {
|
|
33930
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= MAX_TARGET ? value : void 0;
|
|
33931
|
+
}
|
|
33932
|
+
function newNodes() {
|
|
33933
|
+
return Object.create(null);
|
|
33934
|
+
}
|
|
33935
|
+
function readOne(raw) {
|
|
33936
|
+
if (!isRecord(raw)) return null;
|
|
33937
|
+
const facts = {};
|
|
33938
|
+
const status = raw.status;
|
|
33939
|
+
if (typeof status === "string" && STATUSES.includes(status)) facts.status = status;
|
|
33940
|
+
if (typeof raw.synopsis === "string" && raw.synopsis !== "") {
|
|
33941
|
+
let s = raw.synopsis.slice(0, MAX_SYNOPSIS);
|
|
33942
|
+
const last = s.charCodeAt(s.length - 1);
|
|
33943
|
+
if (last >= 55296 && last <= 56319) s = s.slice(0, -1);
|
|
33944
|
+
facts.synopsis = s;
|
|
33945
|
+
}
|
|
33946
|
+
const t = target(raw.target);
|
|
33947
|
+
if (t !== void 0) facts.target = t;
|
|
33948
|
+
return Object.keys(facts).length > 0 ? facts : null;
|
|
33949
|
+
}
|
|
33950
|
+
/** The facts kept in a document's metadata comment; empty when there are none or they are unreadable. */
|
|
33951
|
+
function readFacts(text) {
|
|
33952
|
+
const out = { nodes: newNodes() };
|
|
33953
|
+
let novel;
|
|
33954
|
+
try {
|
|
33955
|
+
novel = readMarkdownMeta(text).meta?.novel;
|
|
33956
|
+
} catch {
|
|
33957
|
+
return out;
|
|
33958
|
+
}
|
|
33959
|
+
if (!isRecord(novel)) return out;
|
|
33960
|
+
const bookTarget = target(novel.bookTarget);
|
|
33961
|
+
if (bookTarget !== void 0) out.bookTarget = bookTarget;
|
|
33962
|
+
if (isRecord(novel.nodes)) for (const id of Object.keys(novel.nodes)) {
|
|
33963
|
+
const facts = readOne(novel.nodes[id]);
|
|
33964
|
+
if (facts) out.nodes[id] = facts;
|
|
33965
|
+
}
|
|
33966
|
+
return out;
|
|
33967
|
+
}
|
|
33700
33968
|
// src/formats.ts
|
|
33701
33969
|
const NO_BYTES = /* @__PURE__ */ new Uint8Array();
|
|
33702
33970
|
/**
|
|
@@ -33713,6 +33981,45 @@ function formatOf(name) {
|
|
|
33713
33981
|
return importKindOf(name) ?? reopenFormatOf(name, NO_BYTES);
|
|
33714
33982
|
}
|
|
33715
33983
|
// src/load.ts
|
|
33984
|
+
/** Whether the text's metadata comment names the novel template (Drafter's own file kind). */
|
|
33985
|
+
function isNovelText(text) {
|
|
33986
|
+
try {
|
|
33987
|
+
return readMarkdownMeta(text).meta?.template === "novel";
|
|
33988
|
+
} catch {
|
|
33989
|
+
return false;
|
|
33990
|
+
}
|
|
33991
|
+
}
|
|
33992
|
+
/**
|
|
33993
|
+
* The title of a unit with none of its own. An untitled section is the one a scene break starts,
|
|
33994
|
+
* which the app calls "Scene break"; a part or chapter with an empty heading is named by its kind.
|
|
33995
|
+
*/
|
|
33996
|
+
const untitled = (kind) => kind === "section" ? "Scene break" : `(untitled ${kind})`;
|
|
33997
|
+
function flatten(nodes, into) {
|
|
33998
|
+
for (const node of nodes) {
|
|
33999
|
+
into.push(node);
|
|
34000
|
+
flatten(node.children, into);
|
|
34001
|
+
}
|
|
34002
|
+
return into;
|
|
34003
|
+
}
|
|
34004
|
+
/**
|
|
34005
|
+
* A novel's units as comparison anchors (their heading lines from 0, as `markdownAnchors` gives
|
|
34006
|
+
* for plain Markdown), so drafts and compares number them as `read_scene` does.
|
|
34007
|
+
*/
|
|
34008
|
+
function novelAnchors(text) {
|
|
34009
|
+
const { starts } = splitLines(text);
|
|
34010
|
+
let line = 0;
|
|
34011
|
+
return novelNodes(text).map((node) => {
|
|
34012
|
+
while (line + 1 < starts.length && starts[line + 1] <= node.heading.from) line++;
|
|
34013
|
+
return {
|
|
34014
|
+
line,
|
|
34015
|
+
title: node.title === "" ? untitled(node.kind) : node.title
|
|
34016
|
+
};
|
|
34017
|
+
});
|
|
34018
|
+
}
|
|
34019
|
+
/** A novel's parts, chapters, and sections in document order: the units every tool numbers. */
|
|
34020
|
+
function novelNodes(text) {
|
|
34021
|
+
return flatten(novelStructure(text).nodes, []);
|
|
34022
|
+
}
|
|
33716
34023
|
const NOT_READ = "Drafter for Claude doesn’t read that kind of file. It reads Fountain, plain text, Markdown, Final Draft, Highland, and Fade In files, and Drafter’s HTML and EPUB exports.";
|
|
33717
34024
|
const SCREENPLAY$2 = builtins["us-screenplay"];
|
|
33718
34025
|
let parses = 0;
|
|
@@ -33807,9 +34114,33 @@ function loadScript(file, format, content, text, importNotes, wordsPerMinute) {
|
|
|
33807
34114
|
importNotes
|
|
33808
34115
|
};
|
|
33809
34116
|
}
|
|
34117
|
+
function loadNovel(file, format, content, text, end) {
|
|
34118
|
+
const nodes = novelNodes(text);
|
|
34119
|
+
const facts = readFacts(text).nodes;
|
|
34120
|
+
return {
|
|
34121
|
+
kind: "markdown",
|
|
34122
|
+
file,
|
|
34123
|
+
format,
|
|
34124
|
+
content,
|
|
34125
|
+
text,
|
|
34126
|
+
title: markdownTitle(text) ?? null,
|
|
34127
|
+
sections: nodes.map((node, i) => ({
|
|
34128
|
+
title: node.title === "" ? untitled(node.kind) : node.title,
|
|
34129
|
+
from: node.heading.from,
|
|
34130
|
+
to: Math.min(end, nodes[i + 1]?.heading.from ?? end)
|
|
34131
|
+
})),
|
|
34132
|
+
novel: nodes.map((node) => ({
|
|
34133
|
+
kind: node.kind,
|
|
34134
|
+
words: node.words,
|
|
34135
|
+
ownWords: node.ownWords,
|
|
34136
|
+
...facts[node.id]
|
|
34137
|
+
}))
|
|
34138
|
+
};
|
|
34139
|
+
}
|
|
33810
34140
|
function loadMarkdown(file, format, content, text) {
|
|
33811
34141
|
const meta = findMarkdownMeta(text);
|
|
33812
34142
|
const end = meta ? meta.from : text.length;
|
|
34143
|
+
if (isNovelText(text)) return loadNovel(file, format, content, text, end);
|
|
33813
34144
|
const { starts } = splitLines(text);
|
|
33814
34145
|
const anchors = markdownAnchors(text).filter((anchor) => (starts[anchor.line] ?? 0) < end);
|
|
33815
34146
|
const sections = anchors.map((anchor, i) => {
|
|
@@ -33827,7 +34158,8 @@ function loadMarkdown(file, format, content, text) {
|
|
|
33827
34158
|
content,
|
|
33828
34159
|
text,
|
|
33829
34160
|
title: markdownTitle(text) ?? null,
|
|
33830
|
-
sections
|
|
34161
|
+
sections,
|
|
34162
|
+
novel: null
|
|
33831
34163
|
};
|
|
33832
34164
|
}
|
|
33833
34165
|
/** The file a tool's path names, read and parsed (or the cached parse, while it is unchanged). */
|
|
@@ -34199,10 +34531,14 @@ function visibleMarkdownIn(text) {
|
|
|
34199
34531
|
}
|
|
34200
34532
|
// ../history/src/print.ts
|
|
34201
34533
|
const SCREENPLAY$1 = builtins["us-screenplay"];
|
|
34202
|
-
/**
|
|
34534
|
+
/**
|
|
34535
|
+
* The template `doc` names in its settings, else the screenplay. Only a built-in's own name: a
|
|
34536
|
+
* script naming `constructor` or `__proto__` got the object's prototype, and Cuts and History
|
|
34537
|
+
* stopped reading it (red/blue audit, 2026-11-03).
|
|
34538
|
+
*/
|
|
34203
34539
|
function templateOf(doc) {
|
|
34204
34540
|
const id = doc.meta?.template;
|
|
34205
|
-
return
|
|
34541
|
+
return typeof id === "string" && Object.hasOwn(builtins, id) ? builtins[id] : SCREENPLAY$1;
|
|
34206
34542
|
}
|
|
34207
34543
|
/**
|
|
34208
34544
|
* A heading folded for matching: upper case, the locked number (`#12A#`) dropped, every dash
|
|
@@ -35942,7 +36278,24 @@ const overviewOutput = object({
|
|
|
35942
36278
|
sections: array(object({
|
|
35943
36279
|
n: number$1().int(),
|
|
35944
36280
|
title: string(),
|
|
35945
|
-
|
|
36281
|
+
/** A novel's: the whole passage, its sections and chapters included. */
|
|
36282
|
+
words: number$1().int(),
|
|
36283
|
+
/** A novel's: the words before its first section or chapter. */
|
|
36284
|
+
own_words: number$1().int().optional(),
|
|
36285
|
+
kind: _enum([
|
|
36286
|
+
"part",
|
|
36287
|
+
"chapter",
|
|
36288
|
+
"section"
|
|
36289
|
+
]).optional(),
|
|
36290
|
+
status: _enum([
|
|
36291
|
+
"todo",
|
|
36292
|
+
"draft",
|
|
36293
|
+
"revised",
|
|
36294
|
+
"done"
|
|
36295
|
+
]).optional(),
|
|
36296
|
+
synopsis: string().optional(),
|
|
36297
|
+
synopsis_cut: literal(true).optional(),
|
|
36298
|
+
target: number$1().int().optional()
|
|
35946
36299
|
})).optional(),
|
|
35947
36300
|
words: number$1().int().optional(),
|
|
35948
36301
|
left_out: leftOut,
|
|
@@ -36004,12 +36357,26 @@ function bounded(items, share, more) {
|
|
|
36004
36357
|
const sectionWords = (text) => wordsIn(text.replace(/^ {0,3}#{1,3}(?=[ \t]|$)/, "").replace(/[ \t]+#+[ \t]*(?=\n|$)/, ""));
|
|
36005
36358
|
function markdownOverview(loaded, args, ctx, include) {
|
|
36006
36359
|
const { text, sections } = loaded;
|
|
36007
|
-
const
|
|
36008
|
-
|
|
36009
|
-
|
|
36010
|
-
|
|
36011
|
-
|
|
36012
|
-
|
|
36360
|
+
const shown = visibleMarkdownIn(text);
|
|
36361
|
+
const own = sections.map((section) => sectionWords(shown(section.from, section.to)));
|
|
36362
|
+
const items = sections.map((section, i) => {
|
|
36363
|
+
const unit = loaded.novel?.[i];
|
|
36364
|
+
const synopsis = unit?.synopsis === void 0 ? void 0 : clipText(unit.synopsis, TEXT_LIMITS.synopsis);
|
|
36365
|
+
return {
|
|
36366
|
+
n: i + 1,
|
|
36367
|
+
title: clipText(section.title, TEXT_LIMITS.heading).text,
|
|
36368
|
+
words: unit ? unit.words : own[i],
|
|
36369
|
+
...unit ? {
|
|
36370
|
+
kind: unit.kind,
|
|
36371
|
+
own_words: unit.ownWords,
|
|
36372
|
+
...unit.status === void 0 ? {} : { status: unit.status },
|
|
36373
|
+
...synopsis ? { synopsis: synopsis.text } : {},
|
|
36374
|
+
...synopsis?.cut ? { synopsis_cut: true } : {},
|
|
36375
|
+
...unit.target === void 0 ? {} : { target: unit.target }
|
|
36376
|
+
} : {}
|
|
36377
|
+
};
|
|
36378
|
+
});
|
|
36379
|
+
const words = wordsIn(shown(0, sections[0]?.from ?? text.length)) + items.reduce((sum, _s, i) => sum + own[i], 0);
|
|
36013
36380
|
const now = cursorBase("script_overview", loaded, {
|
|
36014
36381
|
script: args.script,
|
|
36015
36382
|
include
|
|
@@ -36173,7 +36540,7 @@ async function overview(args, ctx) {
|
|
|
36173
36540
|
const scriptOverview = {
|
|
36174
36541
|
name: "script_overview",
|
|
36175
36542
|
title: "Script overview",
|
|
36176
|
-
description: "The shape of one script: its title page (never the contact), template, pages, and running time; its acts with their synopses; every scene with its heading, place, time of day, act, page, length, synopsis, and who is in it; every character’s speeches, words, share of the talking, scenes, long absences, and who they are most often with; its places; what is still to be written; and a count of problems. For a Markdown document, its title and sections. Notes and the boneyard are left out unless include asks for them. Pass `next` back as `cursor` for more scenes. A field ending `_cut` means its text was too long and only its start is shown; one ending `_more` counts what didn’t fit.",
|
|
36543
|
+
description: "The shape of one script: its title page (never the contact), template, pages, and running time; its acts with their synopses; every scene with its heading, place, time of day, act, page, length, synopsis, and who is in it; every character’s speeches, words, share of the talking, scenes, long absences, and who they are most often with; its places; what is still to be written; and a count of problems. For a Markdown document, its title and sections; for a novel, its parts, chapters, and sections, each with its words and the status, synopsis, and word target the writer keeps for it. Notes and the boneyard are left out unless include asks for them. Pass `next` back as `cursor` for more scenes. A field ending `_cut` means its text was too long and only its start is shown; one ending `_more` counts what didn’t fit.",
|
|
36177
36544
|
input: overviewInput,
|
|
36178
36545
|
output: overviewOutput,
|
|
36179
36546
|
run: overview
|
|
@@ -36609,7 +36976,8 @@ const listed = object({
|
|
|
36609
36976
|
kind: _enum([
|
|
36610
36977
|
"screenplay",
|
|
36611
36978
|
"stage play",
|
|
36612
|
-
"markdown"
|
|
36979
|
+
"markdown",
|
|
36980
|
+
"novel"
|
|
36613
36981
|
]).nullable(),
|
|
36614
36982
|
title: string().nullable(),
|
|
36615
36983
|
/** The title was longer than TEXT_LIMITS.heading, and is its start. */
|
|
@@ -36667,11 +37035,12 @@ function scriptSummary(content, text) {
|
|
|
36667
37035
|
};
|
|
36668
37036
|
}
|
|
36669
37037
|
function markdownSummary(content, text) {
|
|
37038
|
+
const novel = isNovelText(text);
|
|
36670
37039
|
return {
|
|
36671
37040
|
content,
|
|
36672
|
-
kind: "markdown",
|
|
37041
|
+
kind: novel ? "novel" : "markdown",
|
|
36673
37042
|
title: markdownTitle(text) ?? null,
|
|
36674
|
-
scenes: markdownAnchors(text).length
|
|
37043
|
+
scenes: novel ? novelNodes(text).length : markdownAnchors(text).length
|
|
36675
37044
|
};
|
|
36676
37045
|
}
|
|
36677
37046
|
const tooBig$1 = () => new Refused("it is too big");
|
|
@@ -36875,7 +37244,7 @@ async function describe(file, ctx, spend) {
|
|
|
36875
37244
|
const listScripts = {
|
|
36876
37245
|
name: "list_scripts",
|
|
36877
37246
|
title: "List scripts",
|
|
36878
|
-
description: "Lists the scripts, plays, and documents in the writer’s folders (or one folder, given as a path that starts with a folder’s name): each file’s path, kind, title, scene count, date, size, and how many drafts and cuts Drafter keeps beside it, with a manifest: what Drafter found inside each file (`content`), whether that matches its name (`matches_name`), and why Drafter won’t read it (`refused`). Pass `next` back as `cursor` for the rest.",
|
|
37247
|
+
description: "Lists the scripts, plays, and documents in the writer’s folders (or one folder, given as a path that starts with a folder’s name): each file’s path, kind (screenplay, stage play, novel, or markdown), title, scene count, date, size, and how many drafts and cuts Drafter keeps beside it, with a manifest: what Drafter found inside each file (`content`), whether that matches its name (`matches_name`), and why Drafter won’t read it (`refused`). Pass `next` back as `cursor` for the rest.",
|
|
36879
37248
|
input,
|
|
36880
37249
|
output,
|
|
36881
37250
|
run
|
|
@@ -37955,11 +38324,11 @@ const draftsOutput = object({
|
|
|
37955
38324
|
});
|
|
37956
38325
|
const draftSummaries = new LoadCache(999);
|
|
37957
38326
|
/** A draft's pages, scenes, and words: from its text, never returned. */
|
|
37958
|
-
function figures(text, kind) {
|
|
38327
|
+
function figures(text, kind, novel = false) {
|
|
37959
38328
|
if (kind === "markdown") return {
|
|
37960
38329
|
pages: null,
|
|
37961
|
-
scenes: markdownAnchors(text).length,
|
|
37962
|
-
words: wordsIn(text)
|
|
38330
|
+
scenes: (novel ? novelAnchors(text) : markdownAnchors(text)).length,
|
|
38331
|
+
words: wordsIn(markdownProse(text))
|
|
37963
38332
|
};
|
|
37964
38333
|
const doc = parse(text);
|
|
37965
38334
|
const template = templateFor(doc);
|
|
@@ -37972,7 +38341,7 @@ function figures(text, kind) {
|
|
|
37972
38341
|
}
|
|
37973
38342
|
/** Whether a draft's summary is kept already (so reading it again costs nothing). */
|
|
37974
38343
|
const summarised = (file) => draftSummaries.get(file.real, file.size, file.mtimeMs) !== void 0;
|
|
37975
|
-
async function summaryOf(file, kind) {
|
|
38344
|
+
async function summaryOf(file, kind, novel = false) {
|
|
37976
38345
|
const cached = draftSummaries.get(file.real, file.size, file.mtimeMs);
|
|
37977
38346
|
if (cached !== void 0) return cached;
|
|
37978
38347
|
const read = await readDraft(file, kind);
|
|
@@ -37993,7 +38362,7 @@ async function summaryOf(file, kind) {
|
|
|
37993
38362
|
scenes: null,
|
|
37994
38363
|
words: null,
|
|
37995
38364
|
figures: false
|
|
37996
|
-
} : figures(read.text, kind),
|
|
38365
|
+
} : figures(read.text, kind, novel),
|
|
37997
38366
|
matches: read.matches
|
|
37998
38367
|
} };
|
|
37999
38368
|
}
|
|
@@ -38003,6 +38372,7 @@ async function summaryOf(file, kind) {
|
|
|
38003
38372
|
async function listDrafts(args, ctx) {
|
|
38004
38373
|
const loaded = await load(args.script, ctx);
|
|
38005
38374
|
const kind = kindOf(loaded);
|
|
38375
|
+
const isNovel = loaded.kind === "markdown" && loaded.novel !== null;
|
|
38006
38376
|
const files = await draftFiles(loaded, ctx);
|
|
38007
38377
|
const now = cursorBase("list_drafts", loaded, {
|
|
38008
38378
|
script: args.script,
|
|
@@ -38033,7 +38403,7 @@ async function listDrafts(args, ctx) {
|
|
|
38033
38403
|
const size = fresh ? checked.size : 0;
|
|
38034
38404
|
const parse = fresh && checked.size <= 5242880 ? checked.size : 0;
|
|
38035
38405
|
if (examined > 0 && (read + size > PAGE_READ_BYTES || parsed > 0 && parsed + parse > PAGE_PARSE_BYTES)) break;
|
|
38036
|
-
const summary = checked ? await summaryOf(checked, kind) : null;
|
|
38406
|
+
const summary = checked ? await summaryOf(checked, kind, isNovel) : null;
|
|
38037
38407
|
read += size;
|
|
38038
38408
|
parsed += parse;
|
|
38039
38409
|
if (!summary) {
|
|
@@ -38231,7 +38601,7 @@ async function compareDrafts(args, ctx) {
|
|
|
38231
38601
|
left.notes += shown.leftOut.notes;
|
|
38232
38602
|
left.boneyard += shown.leftOut.boneyard;
|
|
38233
38603
|
}
|
|
38234
|
-
const anchorsOf = kind === "markdown" ?
|
|
38604
|
+
const anchorsOf = kind !== "markdown" ? sceneAnchors : loaded.kind === "markdown" && loaded.novel !== null ? novelAnchors : markdownAnchors;
|
|
38235
38605
|
const oldAnchors = anchorsOf(shownOld.text);
|
|
38236
38606
|
const newAnchors = anchorsOf(shownNew.text);
|
|
38237
38607
|
const { rows } = diffRows(shownOld.text, shownNew.text, {
|
package/package.json
CHANGED