texlite 0.9.1 → 0.9.3

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 (95) hide show
  1. package/DESIGN.md +36 -6
  2. package/OPERATIONS.md +70 -3
  3. package/README.md +11 -3
  4. package/README.zh-CN.md +11 -2
  5. package/THIRD_PARTY_NOTICES.md +29 -0
  6. package/dist/client/assets/AdminUsers-huwQK7Ly.js +1 -0
  7. package/dist/client/assets/CitationLibraryDialog-C5C3sPdw.js +1 -0
  8. package/dist/client/assets/Dashboard-DtHSc55D.js +2 -0
  9. package/dist/client/assets/{GitDialog-C9Loz4Al.js → GitDialog-BI7QIMsh.js} +1 -1
  10. package/dist/client/assets/{HistoryDialog-3UNN_jhU.js → HistoryDialog-DA7O9aF2.js} +1 -1
  11. package/dist/client/assets/LatexEditor-BYWWx5Km.js +138 -0
  12. package/dist/client/assets/{PdfPreview-BKhcS-2c.js → PdfPreview-mO9WS01q.js} +1 -1
  13. package/dist/client/assets/ProjectIconPickerDialog-B0q_r5dX.js +1 -0
  14. package/dist/client/assets/{ProjectNavigationDialogs-CZKTnxrl.js → ProjectNavigationDialogs-DwWYbB1A.js} +1 -1
  15. package/dist/client/assets/ProjectSettings-BpPFgdoj.js +1 -0
  16. package/dist/client/assets/ProjectWorkspace-CI2zKon5.js +32 -0
  17. package/dist/client/assets/{SelectionHistoryDialog-D3dO83lg.js → SelectionHistoryDialog-pWMI5izu.js} +2 -2
  18. package/dist/client/assets/SystemMetricsDialog-BmlO7oNg.js +1 -0
  19. package/dist/client/assets/TagManagementDialog-h4Ves4FR.js +1 -0
  20. package/dist/client/assets/{WordCountDialog-TwpyAP3m.js → WordCountDialog-DlBSP4y7.js} +1 -1
  21. package/dist/client/assets/check-BAdi_Lxq.js +1 -0
  22. package/dist/client/assets/editor-DZS7-wrd.js +14 -0
  23. package/dist/client/assets/{hard-drive-XHSiS_A7.js → hard-drive-DP2OHKMC.js} +1 -1
  24. package/dist/client/assets/index-CBrIrzgg.css +1 -0
  25. package/dist/client/assets/{index-Cd6-F29b.js → index-i5IY_PMw.js} +1 -1
  26. package/dist/client/assets/index-teah8iB2.js +12 -0
  27. package/dist/client/assets/katex-D9PXdmFF.css +1 -0
  28. package/dist/client/assets/{key-round-C-CXCWnz.js → key-round-D8sCqL2b.js} +1 -1
  29. package/dist/client/assets/{latexFormatterWorker-DYkYtcFy.js → latexFormatterWorker-BRmsns8a.js} +2 -2
  30. package/dist/client/assets/{minimize-2-BMDPVaF-.js → minimize-2-6tf519Ts.js} +1 -1
  31. package/dist/client/assets/{minus-BIa7-KuM.js → minus-CqvESp9M.js} +1 -1
  32. package/dist/client/assets/{pdf-BB1XvBIF.js → pdf-DUYcbPMK.js} +1 -1
  33. package/dist/client/assets/{plus-BKDWy4Ui.js → plus-DIrweesj.js} +1 -1
  34. package/dist/client/assets/{refresh-cw-Bo7-a7-I.js → refresh-cw-BDcwKu_d.js} +1 -1
  35. package/dist/client/assets/{search-Bz9Ob5mT.js → search-C9_Jyrnx.js} +1 -1
  36. package/dist/client/assets/{sigma-DxlHf9gv.js → sigma-p9-fBP0d.js} +1 -1
  37. package/dist/client/assets/spellCheck-Y0RlmXbl.js +1 -0
  38. package/dist/client/assets/{tags-IMZehjzE.js → tags-DjF3z21M.js} +1 -1
  39. package/dist/client/assets/{type-DTWpFQNQ.js → type-CNrDJnzR.js} +1 -1
  40. package/dist/client/assets/{user-plus--aVpd8K6.js → user-plus-C7exNawX.js} +1 -1
  41. package/dist/client/assets/x-BEVz0wdw.js +1 -0
  42. package/dist/client/index.html +6 -4
  43. package/dist/server/app.js +130 -83
  44. package/dist/server/cli.js +4 -2
  45. package/dist/server/collaboration.js +6 -19
  46. package/dist/server/compileArtifacts.js +3 -2
  47. package/dist/server/config.js +27 -0
  48. package/dist/server/db.js +91 -10
  49. package/dist/server/index.js +4 -2
  50. package/dist/server/instanceLock.js +4 -2
  51. package/dist/server/latexCompletion.js +126 -63
  52. package/dist/server/latexDocumentGraph.js +217 -0
  53. package/dist/server/latexSpellMask.js +3 -4
  54. package/dist/server/pm2.js +3 -2
  55. package/dist/server/projectOutline.js +103 -91
  56. package/dist/server/projectReferences.js +109 -0
  57. package/dist/server/routes/auth.js +5 -2
  58. package/dist/server/routes/comments.js +10 -4
  59. package/dist/server/routes/compile.js +2 -2
  60. package/dist/server/routes/projectFiles.js +12 -3
  61. package/dist/server/routes/projectReferences.js +36 -0
  62. package/dist/server/routes/projectShared.js +33 -12
  63. package/dist/server/routes/projects.js +5 -0
  64. package/dist/server/routes/system.js +1 -0
  65. package/dist/server/zip.js +2 -16
  66. package/dist/shared/basePath.js +44 -0
  67. package/dist/shared/bibtexReferences.js +135 -0
  68. package/dist/shared/collaborationProtocol.js +31 -0
  69. package/dist/shared/latexCommandSnippets.js +91 -0
  70. package/dist/shared/latexDependencies.js +131 -0
  71. package/dist/shared/latexLiterals.js +215 -0
  72. package/dist/shared/latexReferenceCommands.js +75 -0
  73. package/dist/shared/latexReferences.js +193 -0
  74. package/dist/shared/latexRoot.js +21 -0
  75. package/dist/shared/latexScanner.js +183 -0
  76. package/dist/shared/writingChecks.js +4 -0
  77. package/package.json +11 -4
  78. package/texlite.config.example.json +2 -4
  79. package/dist/client/assets/AdminUsers-Dazar9cj.js +0 -1
  80. package/dist/client/assets/CitationLibraryDialog-VSUiSPgG.js +0 -1
  81. package/dist/client/assets/Dashboard-CoE5ESfb.js +0 -2
  82. package/dist/client/assets/LatexEditor-Bq76v8sO.js +0 -65
  83. package/dist/client/assets/ProjectIconPickerDialog-BoWMjQVm.js +0 -1
  84. package/dist/client/assets/ProjectSettings-DiOQVJ40.js +0 -1
  85. package/dist/client/assets/ProjectWorkspace-DqLwkGpT.js +0 -27
  86. package/dist/client/assets/SystemMetricsDialog-0zCwLtH_.js +0 -1
  87. package/dist/client/assets/TagManagementDialog-C6KDwtvd.js +0 -1
  88. package/dist/client/assets/check-sZxOOKtS.js +0 -1
  89. package/dist/client/assets/chevron-left-32SrOwa5.js +0 -1
  90. package/dist/client/assets/editor-CiK7TK0p.js +0 -14
  91. package/dist/client/assets/index-BRr3Kuxh.css +0 -1
  92. package/dist/client/assets/index-BjzELSQ-.js +0 -12
  93. package/dist/client/assets/katex-CEK31ho9.css +0 -1
  94. package/dist/client/assets/spellCheck-BfZEnpwY.js +0 -1
  95. package/dist/client/assets/x-Tbo3aA0J.js +0 -1
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Declarative classification for the small set of LaTeX commands that carry
3
+ * citation keys or cross-reference labels. The generic fallbacks deliberately
4
+ * preserve support for package-defined commands such as `\\smartcite` and
5
+ * `\\myref`, while exact entries cover commands whose arguments differ from
6
+ * their name-based convention.
7
+ */
8
+ const singleCitation = {
9
+ kind: "citation",
10
+ argumentKind: "mandatory",
11
+ maximumArguments: 1
12
+ };
13
+ const multipleCitation = {
14
+ kind: "citation",
15
+ argumentKind: "mandatory",
16
+ maximumArguments: Number.MAX_SAFE_INTEGER
17
+ };
18
+ const singleLabel = {
19
+ kind: "label",
20
+ argumentKind: "mandatory",
21
+ maximumArguments: 1
22
+ };
23
+ const rangeLabel = {
24
+ kind: "label",
25
+ argumentKind: "mandatory",
26
+ maximumArguments: 2
27
+ };
28
+ const optionalLabel = {
29
+ kind: "label",
30
+ argumentKind: "optional",
31
+ maximumArguments: 1
32
+ };
33
+ /**
34
+ * `null` explicitly means a command must never be interpreted as a reference.
35
+ * Several biblatex setup commands contain “cite”, but their braced arguments
36
+ * configure rendering or define commands instead of naming bibliography keys.
37
+ */
38
+ const exactReferenceCommands = {
39
+ href: null,
40
+ hyperref: optionalLabel,
41
+ crefrange: rangeLabel,
42
+ citestyle: null,
43
+ setcitestyle: null,
44
+ newcites: null,
45
+ citetext: null,
46
+ declarecitecommand: null,
47
+ declaremulticitecommand: null,
48
+ declareautocitecommand: null,
49
+ declarecitewrappercommand: null,
50
+ declarecitepunctuation: null,
51
+ declarecitedelimiter: null,
52
+ declarecitedriver: null,
53
+ declarecitealias: null,
54
+ ateverycite: null,
55
+ ateverycitekey: null,
56
+ atnextcite: null,
57
+ atnextcitekey: null
58
+ };
59
+ /**
60
+ * Return the key-bearing argument shape for a command, or null when it is not
61
+ * a reference command. Command names are case-insensitive for classification;
62
+ * the original spelling remains available to callers for display and links.
63
+ */
64
+ export function classifyLatexReferenceCommand(name) {
65
+ const normalized = name.toLowerCase();
66
+ if (Object.prototype.hasOwnProperty.call(exactReferenceCommands, normalized)) {
67
+ return exactReferenceCommands[normalized] ?? null;
68
+ }
69
+ if (normalized === "nocite" || normalized.includes("cite")) {
70
+ return normalized.endsWith("cites") ? multipleCitation : singleCitation;
71
+ }
72
+ if (normalized === "ref" || normalized.endsWith("ref"))
73
+ return singleLabel;
74
+ return null;
75
+ }
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Source-level LaTeX reference scanning. Lexical mechanics, dependency
3
+ * extraction, command classification, and BibTeX key scanning live in their
4
+ * own focused modules; this file only assembles them into editor-facing
5
+ * references and definition targets.
6
+ */
7
+ import { findBibtexEntryKeys } from "./bibtexReferences.js";
8
+ import { maskLatexComments } from "./latexLiterals.js";
9
+ import { forEachLatexCommand, isLatexCommentStart, readLatexBalancedArgument, readLatexMandatoryArguments, readLatexOptionalArguments, skipLatexComment, skipLatexTrivia } from "./latexScanner.js";
10
+ import { classifyLatexReferenceCommand } from "./latexReferenceCommands.js";
11
+ export { findLatexBibliographyFiles, findLatexSourceIncludes } from "./latexDependencies.js";
12
+ export { maskLatexComments } from "./latexLiterals.js";
13
+ function keyRanges(source, argument) {
14
+ const ranges = [];
15
+ // Preserve source offsets while treating comments as whitespace. In
16
+ // particular, a comment after a comma must not become part of the next key.
17
+ const masked = maskLatexComments(source.slice(argument.contentFrom, argument.contentTo));
18
+ let segmentStart = 0;
19
+ for (let index = 0; index <= masked.length; index += 1) {
20
+ if (index !== masked.length && masked[index] !== ",")
21
+ continue;
22
+ let from = segmentStart;
23
+ let to = index;
24
+ while (from < to && /\s/.test(masked[from]))
25
+ from += 1;
26
+ while (to > from && /\s/.test(masked[to - 1]))
27
+ to -= 1;
28
+ if (to > from) {
29
+ const absoluteFrom = argument.contentFrom + from;
30
+ const absoluteTo = argument.contentFrom + to;
31
+ ranges.push({ key: source.slice(absoluteFrom, absoluteTo), from: absoluteFrom, to: absoluteTo });
32
+ }
33
+ segmentStart = index + 1;
34
+ }
35
+ return ranges;
36
+ }
37
+ /**
38
+ * Return the unclosed key argument for a known command shape. Mandatory
39
+ * arguments may be preceded by completed optional arguments; `\\hyperref`
40
+ * is the notable optional-key form.
41
+ */
42
+ function unfinishedReferenceArgument(source, start, spec) {
43
+ let index = start;
44
+ for (let count = 0; count < spec.maximumArguments; count += 1) {
45
+ index = skipLatexTrivia(source, index);
46
+ if (spec.argumentKind === "optional") {
47
+ if (source[index] !== "[")
48
+ return null;
49
+ const optional = readLatexBalancedArgument(source, index, "[", "]");
50
+ if (!optional)
51
+ return index + 1;
52
+ index = optional.to;
53
+ continue;
54
+ }
55
+ while (source[index] === "[") {
56
+ const optional = readLatexBalancedArgument(source, index, "[", "]");
57
+ if (!optional)
58
+ return null;
59
+ index = skipLatexTrivia(source, optional.to);
60
+ }
61
+ if (source[index] !== "{")
62
+ return null;
63
+ const mandatory = readLatexBalancedArgument(source, index, "{", "}");
64
+ if (!mandatory)
65
+ return index + 1;
66
+ index = mandatory.to;
67
+ }
68
+ return null;
69
+ }
70
+ function currentReferenceKey(source, start) {
71
+ let itemStart = start;
72
+ for (let index = start; index < source.length; index += 1) {
73
+ if (isLatexCommentStart(source, index)) {
74
+ index = skipLatexComment(source, index) - 1;
75
+ continue;
76
+ }
77
+ if (source[index] === ",")
78
+ itemStart = index + 1;
79
+ }
80
+ const from = skipLatexTrivia(source, itemStart);
81
+ return { from, query: source.slice(from) };
82
+ }
83
+ function findLatexReferenceCompletion(source, kind) {
84
+ let argumentStart = null;
85
+ forEachLatexCommand(source, (command) => {
86
+ const spec = classifyLatexReferenceCommand(command.name);
87
+ if (!spec || spec.kind !== kind)
88
+ return;
89
+ const active = unfinishedReferenceArgument(source, command.to, spec);
90
+ if (active !== null)
91
+ argumentStart = active;
92
+ });
93
+ return argumentStart === null ? null : currentReferenceKey(source, argumentStart);
94
+ }
95
+ /**
96
+ * Return the active key range while a citation command is still being
97
+ * written. This shares navigation's command classification and lexical rules.
98
+ */
99
+ export function findLatexCitationCompletion(source) {
100
+ return findLatexReferenceCompletion(source, "citation");
101
+ }
102
+ /**
103
+ * Return the active label range while a cross-reference command is being
104
+ * written, including both arguments of `\\crefrange`.
105
+ */
106
+ export function findLatexLabelCompletion(source) {
107
+ return findLatexReferenceCompletion(source, "label");
108
+ }
109
+ /**
110
+ * Return individual keys inside visible citation and cross-reference commands.
111
+ * Nested commands remain visible, for example a citation in a caption option.
112
+ */
113
+ export function findLatexReferences(source) {
114
+ const references = [];
115
+ forEachLatexCommand(source, (command) => {
116
+ const spec = classifyLatexReferenceCommand(command.name);
117
+ if (!spec)
118
+ return;
119
+ const argumentsFound = spec.argumentKind === "optional"
120
+ ? readLatexOptionalArguments(source, command.to, spec.maximumArguments)
121
+ : readLatexMandatoryArguments(source, command.to, spec.maximumArguments);
122
+ for (const argument of argumentsFound) {
123
+ for (const range of keyRanges(source, argument)) {
124
+ references.push({ kind: spec.kind, command: command.name, ...range });
125
+ }
126
+ }
127
+ });
128
+ return references.sort((left, right) => left.from - right.from || left.to - right.to);
129
+ }
130
+ export function latexReferenceAt(source, offset) {
131
+ return findLatexReferences(source).find((reference) => offset >= reference.from && offset < reference.to) ?? null;
132
+ }
133
+ function definitionsFromLatex(source, kind) {
134
+ const definitions = [];
135
+ forEachLatexCommand(source, (command) => {
136
+ if (kind === "label" && (command.name === "label" || command.name === "hypertarget")) {
137
+ const argument = readLatexMandatoryArguments(source, command.to, 1)[0];
138
+ if (!argument)
139
+ return;
140
+ for (const range of keyRanges(source, argument)) {
141
+ definitions.push({
142
+ key: range.key,
143
+ command: command.name,
144
+ targetFrom: command.from,
145
+ targetTo: argument.to,
146
+ source: "label"
147
+ });
148
+ }
149
+ return;
150
+ }
151
+ if (kind === "citation" && command.name === "bibitem") {
152
+ const argument = readLatexMandatoryArguments(source, command.to, 1)[0];
153
+ if (!argument)
154
+ return;
155
+ for (const range of keyRanges(source, argument)) {
156
+ definitions.push({
157
+ key: range.key,
158
+ command: command.name,
159
+ targetFrom: command.from,
160
+ targetTo: argument.to,
161
+ source: "bibitem"
162
+ });
163
+ }
164
+ }
165
+ });
166
+ return definitions;
167
+ }
168
+ function definitionsFromBibtex(source) {
169
+ return findBibtexEntryKeys(source).map((entry) => ({
170
+ key: entry.key,
171
+ command: "bibtex",
172
+ targetFrom: entry.entryFrom,
173
+ targetTo: entry.entryTo,
174
+ source: "bibtex"
175
+ }));
176
+ }
177
+ export function findLatexReferenceDefinitions(source, kind, bibtex = false) {
178
+ if (bibtex)
179
+ return kind === "citation" ? definitionsFromBibtex(source) : [];
180
+ return definitionsFromLatex(source, kind);
181
+ }
182
+ export function lineAndColumnAt(source, offset) {
183
+ const boundedOffset = Math.max(0, Math.min(offset, source.length));
184
+ let line = 1;
185
+ let lineStart = 0;
186
+ for (let index = 0; index < boundedOffset; index += 1) {
187
+ if (source[index] !== "\n")
188
+ continue;
189
+ line += 1;
190
+ lineStart = index + 1;
191
+ }
192
+ return { line, column: boundedOffset - lineStart + 1 };
193
+ }
@@ -0,0 +1,21 @@
1
+ import { maskLatexComments, maskLatexLiteralContent } from "./latexLiterals.js";
2
+ function isEscaped(source, index) {
3
+ let backslashes = 0;
4
+ for (let cursor = index - 1; cursor >= 0 && source[cursor] === "\\"; cursor -= 1)
5
+ backslashes += 1;
6
+ return backslashes % 2 === 1;
7
+ }
8
+ /**
9
+ * Return whether source contains a real `\\documentclass` declaration.
10
+ * Literal code and comments are ignored consistently by browser-side root
11
+ * selection and server-side ZIP import.
12
+ */
13
+ export function hasLatexDocumentClass(source) {
14
+ const visible = maskLatexComments(maskLatexLiteralContent(source));
15
+ const declaration = /\\documentclass(?![A-Za-z@])\s*(?:\[[^\]]*\]\s*)?\{/g;
16
+ for (const match of visible.matchAll(declaration)) {
17
+ if (match.index !== undefined && !isEscaped(visible, match.index))
18
+ return true;
19
+ }
20
+ return false;
21
+ }
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Offset-preserving lexical helpers shared by LaTeX reference, dependency,
3
+ * outline-adjacent, and completion logic. This is intentionally a scanner,
4
+ * not a TeX expansion engine: it understands comments, literal forms, command
5
+ * boundaries, and balanced arguments without assigning package semantics.
6
+ */
7
+ import { inlineLatexLiteralEnd, literalEnvironmentEnd, literalEnvironmentNames } from "./latexLiterals.js";
8
+ const literalEnvironments = new Set(literalEnvironmentNames);
9
+ function isCommandNameCharacter(character) {
10
+ return /[A-Za-z@0-9:_-]/.test(character);
11
+ }
12
+ export function isLatexEscaped(source, index) {
13
+ let backslashes = 0;
14
+ for (let cursor = index - 1; cursor >= 0 && source[cursor] === "\\"; cursor -= 1)
15
+ backslashes += 1;
16
+ return backslashes % 2 === 1;
17
+ }
18
+ export function isLatexCommentStart(source, index) {
19
+ return source[index] === "%" && !isLatexEscaped(source, index);
20
+ }
21
+ export function skipLatexComment(source, index) {
22
+ const newline = source.indexOf("\n", index);
23
+ return newline === -1 ? source.length : newline;
24
+ }
25
+ export function skipLatexTrivia(source, start) {
26
+ let index = start;
27
+ while (index < source.length) {
28
+ if (/\s/.test(source[index])) {
29
+ index += 1;
30
+ continue;
31
+ }
32
+ if (isLatexCommentStart(source, index)) {
33
+ index = skipLatexComment(source, index);
34
+ continue;
35
+ }
36
+ break;
37
+ }
38
+ return index;
39
+ }
40
+ /** Read one balanced braced or bracketed argument without changing offsets. */
41
+ export function readLatexBalancedArgument(source, start, opening, closing) {
42
+ if (source[start] !== opening)
43
+ return null;
44
+ let depth = 0;
45
+ let braceDepth = 0;
46
+ for (let index = start; index < source.length; index += 1) {
47
+ const character = source[index];
48
+ if (character === "\\" && index + 1 < source.length) {
49
+ index += 1;
50
+ continue;
51
+ }
52
+ if (isLatexCommentStart(source, index)) {
53
+ index = skipLatexComment(source, index) - 1;
54
+ continue;
55
+ }
56
+ // Braced content inside an optional argument can contain literal square
57
+ // brackets, which must not close its outer option.
58
+ if (opening === "[") {
59
+ if (character === "{") {
60
+ braceDepth += 1;
61
+ continue;
62
+ }
63
+ if (character === "}" && braceDepth > 0) {
64
+ braceDepth -= 1;
65
+ continue;
66
+ }
67
+ if (braceDepth > 0)
68
+ continue;
69
+ }
70
+ if (character === opening) {
71
+ depth += 1;
72
+ continue;
73
+ }
74
+ if (character !== closing)
75
+ continue;
76
+ depth -= 1;
77
+ if (depth === 0) {
78
+ return {
79
+ from: start,
80
+ to: index + 1,
81
+ contentFrom: start + 1,
82
+ contentTo: index
83
+ };
84
+ }
85
+ }
86
+ return null;
87
+ }
88
+ export function readLatexMandatoryArguments(source, start, maximum) {
89
+ const argumentsFound = [];
90
+ let index = start;
91
+ while (argumentsFound.length < maximum) {
92
+ index = skipLatexTrivia(source, index);
93
+ while (source[index] === "[") {
94
+ const optional = readLatexBalancedArgument(source, index, "[", "]");
95
+ if (!optional)
96
+ return argumentsFound;
97
+ index = skipLatexTrivia(source, optional.to);
98
+ }
99
+ if (source[index] !== "{")
100
+ break;
101
+ const mandatory = readLatexBalancedArgument(source, index, "{", "}");
102
+ if (!mandatory)
103
+ break;
104
+ argumentsFound.push(mandatory);
105
+ index = mandatory.to;
106
+ }
107
+ return argumentsFound;
108
+ }
109
+ export function readLatexOptionalArguments(source, start, maximum) {
110
+ const argumentsFound = [];
111
+ let index = start;
112
+ while (argumentsFound.length < maximum) {
113
+ index = skipLatexTrivia(source, index);
114
+ if (source[index] !== "[")
115
+ break;
116
+ const optional = readLatexBalancedArgument(source, index, "[", "]");
117
+ if (!optional)
118
+ break;
119
+ argumentsFound.push(optional);
120
+ index = optional.to;
121
+ }
122
+ return argumentsFound;
123
+ }
124
+ function readLatexCommand(source, start) {
125
+ if (source[start] !== "\\" || start + 1 >= source.length)
126
+ return null;
127
+ let index = start + 1;
128
+ if (!/[A-Za-z@]/.test(source[index])) {
129
+ return { name: source[index], from: start, to: index + 1 };
130
+ }
131
+ while (index < source.length && isCommandNameCharacter(source[index]))
132
+ index += 1;
133
+ const name = source.slice(start + 1, index);
134
+ if (source[index] === "*")
135
+ index += 1;
136
+ return { name, from: start, to: index };
137
+ }
138
+ function skipLiteralEnvironment(source, command) {
139
+ if (command.name !== "begin")
140
+ return command.to;
141
+ const environment = readLatexMandatoryArguments(source, command.to, 1)[0];
142
+ if (!environment)
143
+ return command.to;
144
+ const name = source.slice(environment.contentFrom, environment.contentTo).trim();
145
+ if (!literalEnvironments.has(name))
146
+ return command.to;
147
+ return literalEnvironmentEnd(source, environment.to, name)?.to ?? source.length;
148
+ }
149
+ /**
150
+ * Visit visible TeX commands in source order. Commands nested in arguments are
151
+ * intentionally still visited; commands in comments and literal source forms
152
+ * are not.
153
+ */
154
+ export function forEachLatexCommand(source, visitor) {
155
+ let index = 0;
156
+ while (index < source.length) {
157
+ if (isLatexCommentStart(source, index)) {
158
+ index = skipLatexComment(source, index);
159
+ continue;
160
+ }
161
+ if (source[index] !== "\\") {
162
+ index += 1;
163
+ continue;
164
+ }
165
+ const command = readLatexCommand(source, index);
166
+ if (!command) {
167
+ index += 1;
168
+ continue;
169
+ }
170
+ const literalEnd = inlineLatexLiteralEnd(source, command.from);
171
+ if (literalEnd !== null && literalEnd > command.to) {
172
+ index = literalEnd;
173
+ continue;
174
+ }
175
+ const environmentEnd = skipLiteralEnvironment(source, command);
176
+ if (environmentEnd > command.to) {
177
+ index = environmentEnd;
178
+ continue;
179
+ }
180
+ visitor(command);
181
+ index = command.to;
182
+ }
183
+ }
@@ -0,0 +1,4 @@
1
+ /** Bibliographies and BibTeX style programs are structured data/code, not prose. */
2
+ export function supportsWritingChecks(path) {
3
+ return !/\.(?:bib|bst)$/i.test(path.trim());
4
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "texlite",
3
- "version": "0.9.1",
3
+ "version": "0.9.3",
4
4
  "description": "A lightweight local-first collaborative LaTeX workspace",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {
@@ -28,6 +28,7 @@
28
28
  "DESIGN.md",
29
29
  "NPM_TESTING.md",
30
30
  "LICENSE",
31
+ "THIRD_PARTY_NOTICES.md",
31
32
  "preview-1.png",
32
33
  "preview-2.png",
33
34
  "texlite.config.example.json"
@@ -41,9 +42,9 @@
41
42
  "type": "module",
42
43
  "scripts": {
43
44
  "dev": "npm run generate:lucide-icons && tsx watch src/server/index.ts",
44
- "dev:web": "vite",
45
+ "dev:web": "npm run generate:bibtex-parser && vite",
45
46
  "build": "npm run build:web && npm run build:server",
46
- "build:web": "vite build",
47
+ "build:web": "npm run generate:bibtex-parser && vite build",
47
48
  "build:server": "tsc -p tsconfig.server.json && npm run generate:lucide-icons",
48
49
  "start": "node dist/server/index.js",
49
50
  "pm2:start": "pm2 start ecosystem.config.cjs",
@@ -55,8 +56,9 @@
55
56
  "init": "tsx src/server/cli.ts init",
56
57
  "prepack": "npm run build",
57
58
  "generate:lucide-icons": "node scripts/generate-lucide-static-icons.mjs",
58
- "pretest": "npm run generate:lucide-icons",
59
+ "pretest": "npm run generate:lucide-icons && npm run generate:bibtex-parser",
59
60
  "test": "vitest run --config vitest.config.ts",
61
+ "generate:bibtex-parser": "node scripts/generate-bibtex-parser.mjs",
60
62
  "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.server.json --noEmit && tsc -p tsconfig.test.json --noEmit"
61
63
  },
62
64
  "dependencies": {
@@ -65,6 +67,7 @@
65
67
  "@codemirror/lang-markdown": "^6.3.3",
66
68
  "@codemirror/language": "^6.11.3",
67
69
  "@codemirror/legacy-modes": "^6.5.1",
70
+ "@codemirror/lint": "^6.9.7",
68
71
  "@codemirror/search": "^6.7.1",
69
72
  "@codemirror/state": "^6.5.2",
70
73
  "@codemirror/view": "^6.38.1",
@@ -72,6 +75,9 @@
72
75
  "@fastify/multipart": "^9.2.1",
73
76
  "@fastify/static": "^10.1.2",
74
77
  "@fastify/websocket": "^11.3.0",
78
+ "@lezer/common": "^1.5.2",
79
+ "@lezer/highlight": "^1.2.3",
80
+ "@lezer/lr": "^1.4.10",
75
81
  "@radix-ui/react-dialog": "^1.1.15",
76
82
  "@replit/codemirror-vim": "^6.4.0",
77
83
  "better-sqlite3": "^13.0.2",
@@ -100,6 +106,7 @@
100
106
  "yjs": "^13.6.31"
101
107
  },
102
108
  "devDependencies": {
109
+ "@lezer/generator": "^1.8.0",
103
110
  "@types/better-sqlite3": "^9.6.0",
104
111
  "@types/diff-match-patch": "^1.0.36",
105
112
  "@types/node": "^24.1.0",
@@ -4,10 +4,8 @@
4
4
  "sessionDays": 14,
5
5
  "server": {
6
6
  "host": "127.0.0.1",
7
- "port": 3000
8
- },
9
- "storage": {
10
- "dataDir": ".texlite"
7
+ "port": 3000,
8
+ "basePath": "/"
11
9
  },
12
10
  "uploads": {
13
11
  "maxFileSizeMB": 50
@@ -1 +0,0 @@
1
- import{u as Y,r as n,j as e}from"./react-BVUjbTTn.js";import{c as h,M as D,b as _,C as ee,f as u,g as m}from"./index-BjzELSQ-.js";import{U as se,T as R,X as O}from"./x-Tbo3aA0J.js";import{U as $}from"./user-plus--aVpd8K6.js";import{S as ae}from"./Dashboard-CoE5ESfb.js";import{K as B}from"./key-round-C-CXCWnz.js";import"./hard-drive-XHSiS_A7.js";import"./sigma-DxlHf9gv.js";import"./tags-IMZehjzE.js";import"./chevron-left-32SrOwa5.js";const re=h("Dices",[["rect",{width:"12",height:"12",x:"2",y:"10",rx:"2",ry:"2",key:"6agr2n"}],["path",{d:"m17.92 14 3.5-3.5a2.24 2.24 0 0 0 0-3l-5-4.92a2.24 2.24 0 0 0-3 0L10 6",key:"1o487t"}],["path",{d:"M6 18h.01",key:"uhywen"}],["path",{d:"M10 14h.01",key:"ssrbsk"}],["path",{d:"M15 6h.01",key:"cblpky"}],["path",{d:"M18 9h.01",key:"2061c0"}]]);const ne=h("FolderCheck",[["path",{d:"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z",key:"1kt360"}],["path",{d:"m9 13 2 2 4-4",key:"6343dt"}]]);const te=h("FolderX",[["path",{d:"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z",key:"1kt360"}],["path",{d:"m9.5 10.5 5 5",key:"ra9qjz"}],["path",{d:"m14.5 10.5-5 5",key:"l2rkpq"}]]);const ie=h("ShieldOff",[["path",{d:"m2 2 20 20",key:"1ooewy"}],["path",{d:"M5 5a1 1 0 0 0-1 1v7c0 5 3.5 7.5 7.67 8.94a1 1 0 0 0 .67.01c2.35-.82 4.48-1.97 5.9-3.71",key:"1jlk70"}],["path",{d:"M9.309 3.652A12.252 12.252 0 0 0 11.24 2.28a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1v7a9.784 9.784 0 0 1-.08 1.264",key:"18rp1v"}]]);const oe=h("UserCheck",[["path",{d:"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2",key:"1yyitq"}],["circle",{cx:"9",cy:"7",r:"4",key:"nufk8"}],["polyline",{points:"16 11 18 13 22 9",key:"1pwet4"}]]);const le=h("UserX",[["path",{d:"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2",key:"1yyitq"}],["circle",{cx:"9",cy:"7",r:"4",key:"nufk8"}],["line",{x1:"17",x2:"22",y1:"8",y2:"13",key:"3nzzx3"}],["line",{x1:"22",x2:"17",y1:"8",y2:"13",key:"1swrse"}]]);function de(p=10){const t="ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789!@#$%",a=crypto.getRandomValues(new Uint8Array(p));return Array.from(a,S=>t[S%t.length]).join("")}function Ce({currentUser:p,minPasswordLength:t}){const{t:a}=Y(),[S,J]=n.useState([]),[E,d]=n.useState(""),[V,C]=n.useState(!1),[r,o]=n.useState({username:"",displayName:"",password:"",role:"user",canCreateProjects:!1}),[k,g]=n.useState(null),[j,P]=n.useState(""),[x,w]=n.useState(null),[z,N]=n.useState(!1),[i,F]=n.useState(!1),[U,y]=n.useState(""),[l,f]=n.useState(null),[A,H]=n.useState(!1),[X,c]=n.useState(""),b=()=>u("/api/admin/users").then(({users:s})=>J(s)).catch(s=>d(m(s)));n.useEffect(()=>{b()},[]);const q=async()=>{if(r.password.length<t)return d(a("auth.passwordMinimum",{count:t}));try{await u("/api/admin/users",{method:"POST",body:JSON.stringify({username:r.username,displayName:r.displayName||r.username,password:r.password,role:r.role,canCreateProjects:r.canCreateProjects})}),C(!1),o({username:"",displayName:"",password:"",role:"user",canCreateProjects:!1}),await b()}catch(s){d(m(s))}},M=async(s,v)=>{try{await u(`/api/admin/users/${s.id}`,{method:"PATCH",body:JSON.stringify(v)}),await b()}catch(T){d(m(T))}},K=async s=>{if(!s.disabled){f({target:s,setting:"disable"}),c("");return}await M(s,{disabled:!1})},Z=async s=>{if(s.role==="admin"){f({target:s,setting:"demote"}),c("");return}await M(s,{role:"admin"})},G=async s=>{if(s.canCreateProjects){f({target:s,setting:"denyProjectCreation"}),c("");return}await M(s,{canCreateProjects:!0})},I=async()=>{if(!l)return;const{target:s,setting:v}=l,T=v==="disable"?{disabled:!0}:v==="demote"?{role:"user"}:{canCreateProjects:!1};H(!0),c("");try{await u(`/api/admin/users/${s.id}`,{method:"PATCH",body:JSON.stringify(T)}),await b(),f(null)}catch(W){c(m(W))}finally{H(!1)}},L=async()=>{if(!(!k||!j)){if(j.length<t)return d(a("auth.passwordMinimum",{count:t}));try{await u(`/api/admin/users/${k.id}`,{method:"PATCH",body:JSON.stringify({password:j})}),g(null),P("")}catch(s){d(m(s))}}},Q=async()=>{if(!(!x||i)){F(!0),y("");try{await u(`/api/admin/users/${x.id}`,{method:"DELETE",body:JSON.stringify({deleteProjects:z})}),w(null),N(!1),await b()}catch(s){y(m(s))}finally{F(!1)}}};return e.jsxs("main",{className:"dashboard",children:[e.jsxs("div",{className:"section-title",children:[e.jsxs("div",{children:[e.jsxs("h1",{children:[e.jsx(se,{"aria-hidden":!0,size:25}),a("users.manage")]}),e.jsx("p",{className:"muted",children:a("users.onlyAdmin")})]}),e.jsxs("button",{className:"primary icon-button",onClick:()=>C(!0),children:[e.jsx($,{"aria-hidden":!0,size:15}),a("users.add")]})]}),E&&e.jsx("p",{className:"error",children:E}),e.jsx("div",{className:"table-card",children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:a("common.user")}),e.jsx("th",{children:a("users.role")}),e.jsx("th",{children:a("users.createProjects")}),e.jsx("th",{children:a("users.ownedProjects")}),e.jsx("th",{children:a("users.status")}),e.jsx("th",{children:a("users.actions")})]})}),e.jsx("tbody",{children:S.map(s=>e.jsxs("tr",{children:[e.jsxs("td",{children:[e.jsx("strong",{children:s.displayName}),e.jsxs("small",{children:["@",s.username]})]}),e.jsx("td",{children:s.role==="admin"?a("common.admin"):a("common.user")}),e.jsx("td",{children:s.canCreateProjects?a("users.allow"):a("users.deny")}),e.jsx("td",{children:s.ownedProjects}),e.jsx("td",{children:s.disabled?a("common.disabled"):a("common.normal")}),e.jsxs("td",{children:[e.jsxs("button",{className:"icon-button",disabled:s.id===p.id,onClick:()=>K(s),children:[s.disabled?e.jsx(oe,{"aria-hidden":!0,size:13}):e.jsx(le,{"aria-hidden":!0,size:13}),s.disabled?a("users.enable"):a("users.disable")]}),e.jsxs("button",{className:"icon-button",disabled:s.id===p.id,onClick:()=>Z(s),children:[s.role==="admin"?e.jsx(ie,{"aria-hidden":!0,size:13}):e.jsx(ae,{"aria-hidden":!0,size:13}),s.role==="admin"?a("users.demote"):a("users.promote")]}),e.jsxs("button",{className:"icon-button",disabled:s.role==="admin",onClick:()=>G(s),children:[s.canCreateProjects?e.jsx(te,{"aria-hidden":!0,size:13}):e.jsx(ne,{"aria-hidden":!0,size:13}),s.canCreateProjects?a("users.denyCreate"):a("users.allowCreate")]}),e.jsxs("button",{className:"icon-button",onClick:()=>{g(s),P("")},children:[e.jsx(B,{"aria-hidden":!0,size:13}),a("users.resetPassword")]}),e.jsxs("button",{className:"icon-button danger-text",disabled:s.id===p.id,onClick:()=>{w(s),N(!1),y("")},children:[e.jsx(R,{"aria-hidden":!0,size:13}),a("common.delete")]})]})]},s.id))})]})}),e.jsx(D,{open:V,title:a("users.add"),description:a("users.addDescription"),onOpenChange:C,footer:e.jsxs(e.Fragment,{children:[e.jsxs("button",{className:"icon-button",onClick:()=>C(!1),children:[e.jsx(O,{"aria-hidden":!0,size:14}),a("common.cancel")]}),e.jsxs("button",{className:"primary icon-button",disabled:r.password.length<t,onClick:()=>{q()},children:[e.jsx($,{"aria-hidden":!0,size:14}),a("users.createUser")]})]}),children:e.jsxs("div",{className:"form-stack",children:[e.jsxs("label",{className:"form-field",children:[a("auth.username"),e.jsx("input",{value:r.username,onChange:s=>o({...r,username:s.target.value})})]}),e.jsxs("label",{className:"form-field",children:[a("users.displayName"),e.jsx("input",{value:r.displayName,onChange:s=>o({...r,displayName:s.target.value})})]}),e.jsxs("label",{className:"form-field",children:[a("users.initialPassword"),e.jsxs("span",{className:"password-generator",children:[e.jsx("input",{minLength:t,autoComplete:"new-password",value:r.password,onChange:s=>o({...r,password:s.target.value})}),e.jsxs("button",{type:"button",title:a("users.generatePassword"),onClick:()=>o({...r,password:de()}),children:[e.jsx(re,{size:15}),a("users.randomPassword")]})]}),e.jsx("small",{className:"field-hint",children:a("auth.passwordMinimum",{count:t})})]}),e.jsxs("label",{className:"form-field",children:[a("users.role"),e.jsxs("select",{value:r.role,onChange:s=>o({...r,role:s.target.value}),children:[e.jsx("option",{value:"user",children:a("common.user")}),e.jsx("option",{value:"admin",children:a("common.admin")})]})]}),e.jsxs("label",{className:"checkbox-field",children:[e.jsx("input",{type:"checkbox",checked:r.canCreateProjects||r.role==="admin",disabled:r.role==="admin",onChange:s=>o({...r,canCreateProjects:s.target.checked})})," ",a("users.allowCreate")]})]})}),e.jsx(D,{open:!!k,title:a("users.resetPassword"),description:a("users.resetDescription",{username:k?.username??""}),onOpenChange:s=>{s||g(null)},footer:e.jsxs(e.Fragment,{children:[e.jsxs("button",{className:"icon-button",onClick:()=>g(null),children:[e.jsx(O,{"aria-hidden":!0,size:14}),a("common.cancel")]}),e.jsxs("button",{className:"primary icon-button",disabled:j.length<t,onClick:()=>{L()},children:[e.jsx(B,{"aria-hidden":!0,size:14}),a("users.reset")]})]}),children:e.jsxs("label",{className:"form-field",children:[a("auth.newPassword"),e.jsx("input",{autoFocus:!0,type:"password",minLength:t,autoComplete:"new-password",value:j,onChange:s=>P(s.target.value)}),e.jsx("small",{className:"field-hint",children:a("auth.passwordMinimum",{count:t})})]})}),e.jsx(D,{open:!!x,title:a("users.deleteTitle"),description:a("users.deleteDescription",{username:x?.username??""}),onOpenChange:s=>{!s&&!i&&(w(null),y(""))},footer:e.jsxs(e.Fragment,{children:[e.jsxs("button",{className:"icon-button",disabled:i,onClick:()=>{w(null),y("")},children:[e.jsx(O,{"aria-hidden":!0,size:14}),a("common.cancel")]}),e.jsxs("button",{className:"danger icon-button",disabled:i,"aria-busy":i,onClick:()=>{Q()},children:[i?e.jsx(_,{className:"spin","aria-hidden":!0,size:14}):e.jsx(R,{"aria-hidden":!0,size:14}),a(i?"common.loading":"users.deleteTitle")]})]}),children:e.jsxs(e.Fragment,{children:[U&&e.jsx("p",{className:"error dialog-error",children:U}),e.jsxs("fieldset",{className:"choice-group",disabled:i,children:[e.jsx("legend",{children:a("users.ownedChoice",{count:x?.ownedProjects??0})}),e.jsxs("label",{children:[e.jsx("input",{type:"radio",checked:!z,onChange:()=>N(!1)})," ",a("users.transferProjects")]}),e.jsxs("label",{children:[e.jsx("input",{type:"radio",checked:z,onChange:()=>N(!0)})," ",a("users.deleteProjects")]})]})]})}),e.jsx(ee,{open:!!l,title:a(`userConfirmations.${l?.setting??"disable"}Title`),description:a(`userConfirmations.${l?.setting??"disable"}Description`,{username:l?.target.username??""}),confirmLabel:a(`userConfirmations.${l?.setting??"disable"}Confirm`),danger:!0,busy:A,error:X,onCancel:()=>{A||(f(null),c(""))},onConfirm:()=>{I()}})]})}export{Ce as AdminUsers};
@@ -1 +0,0 @@
1
- import{u as Yt,r,j as t}from"./react-BVUjbTTn.js";import{c as At,f as S,g as k,b as H,C as Bt,M as ut}from"./index-BjzELSQ-.js";import{parseBibEntriesResult as Qt,citationBibtexLimitLabel as Xt,parseSingleBibEntry as Kt,citationVenue as Gt}from"./citationLibrary-ZeQPCv5O.js";import{B as tt,X as et,C as Zt,i as zt,T as ti}from"./x-Tbo3aA0J.js";import{P as it}from"./plus-BKDWy4Ui.js";import{C as J}from"./check-sZxOOKtS.js";import{S as ii}from"./search-Bz9Ob5mT.js";import{C as bt}from"./chevron-left-32SrOwa5.js";import{T as ei}from"./tags-IMZehjzE.js";const ai=At("Import",[["path",{d:"M12 3v12",key:"1x0j5s"}],["path",{d:"m8 11 4 4 4-4",key:"1dohi6"}],["path",{d:"M8 5H4a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-4",key:"1ywtjm"}]]);const ni=At("PenLine",[["path",{d:"M12 20h9",key:"t2du7b"}],["path",{d:"M16.376 3.622a1 1 0 0 1 3.002 3.002L7.368 18.635a2 2 0 0 1-.855.506l-2.872.838a.5.5 0 0 1-.62-.62l.838-2.872a2 2 0 0 1 .506-.854z",key:"1ykcvy"}]]),si=[],ri={status:"empty",entries:si};function pi({open:o,onOpenChange:j,page:u=!1,onBack:b,currentFile:v="",currentSource:p="",readOnly:E=!1,currentUserId:m,maxBibtexBytes:l,onInsert:f}){const{t:e}=Yt(),[K,A]=r.useState("library"),[z,d]=r.useState(""),[L,P]=r.useState(""),[I,N]=r.useState([]),[at,C]=r.useState(1),[g,ht]=r.useState({page:1,pageSize:60,total:0,totalPages:0}),[Ft,U]=r.useState(0),[q,pt]=r.useState([]),[Y,gt]=r.useState(""),[xt,_t]=r.useState("green"),[h,x]=r.useState(null),[B,F]=r.useState([]),[nt,jt]=r.useState(!1),[Rt,vt]=r.useState(!1),[V,O]=r.useState(null),[y,T]=r.useState(null),[st,rt]=r.useState(""),[Dt,_]=r.useState(!1),[Q,ot]=r.useState(""),[ft,ct]=r.useState(null),[lt,X]=r.useState(null),[It,W]=r.useState(""),[$,c]=r.useState(""),[Lt,M]=r.useState(""),[Ct,w]=r.useState(new Map),[Ot,G]=r.useState(!1),[$t,Ht]=r.useState(0),R=!!(/\.bib$/i.test(v)&&f),dt=r.useMemo(()=>o&&R?Qt(p,l):ri,[o,p,R,l]),Nt=dt.status==="too-large",Tt=dt.status==="invalid",D=dt.entries,Jt=r.useMemo(()=>new Set(D.map(i=>i.citationKey.toLowerCase())),[D]);r.useEffect(()=>{if(!o)return;const i=new AbortController,a=window.setTimeout(()=>{vt(!0),c("");const n=new URLSearchParams;z.trim()&&n.set("q",z.trim()),L&&n.set("tag",L),n.set("page",String(at)),n.set("pageSize","60"),S(`/api/citations?${n.toString()}`,{signal:i.signal}).then(s=>{N(s.entries),ht(s.pagination),s.pagination.page!==at&&C(s.pagination.page)}).catch(s=>{s instanceof Error&&s.name==="AbortError"||c(k(s))}).finally(()=>{i.signal.aborted||vt(!1)})},z.trim()?180:0);return()=>{window.clearTimeout(a),i.abort()}},[o,z,L,at,Ft]),r.useEffect(()=>{if(!o)return;const i=new AbortController;return S("/api/citations/tags",{signal:i.signal}).then(a=>pt(a.tags)).catch(a=>{a instanceof Error&&a.name==="AbortError"||c(k(a))}),()=>i.abort()},[o]),r.useEffect(()=>{if(!o||!R){w(new Map),G(!1);return}const i=new AbortController,a=[...new Set(D.map(n=>n.citationKey))];return a.length?(G(!0),S("/api/citations/lookup",{method:"POST",body:JSON.stringify({keys:a}),signal:i.signal}).then(n=>{w(new Map(n.matches.map(s=>[s.citationKey.toLowerCase(),s])))}).catch(n=>{n instanceof Error&&n.name==="AbortError"||c(k(n))}).finally(()=>{i.signal.aborted||G(!1)}),()=>i.abort()):(w(new Map),G(!1),()=>i.abort())},[o,R,D,$t]),r.useEffect(()=>{o&&(A("library"),d(""),P(""),C(1),ht({page:1,pageSize:60,total:0,totalPages:0}),T(null),_(!1),ot(""),x(null),ct(null),X(null),W(""),c(""),M(""),w(new Map))},[o,R]);const Ut=async i=>{O(i.citationKey),c("");try{const a=Ct.get(i.citationKey.toLowerCase()),n=await S("/api/citations",{method:"POST",body:JSON.stringify({bibtex:i.bibtex,citationKey:i.citationKey,entryType:i.entryType,title:i.title,authors:i.authors,year:i.year,overwrite:!!a,expectedRevision:a?.revision})});N(s=>[n.entry,...s.filter(Z=>Z.id!==n.entry.id&&Z.citationKey.toLowerCase()!==n.entry.citationKey.toLowerCase())]),w(s=>new Map(s).set(n.entry.citationKey.toLowerCase(),{id:n.entry.id,citationKey:n.entry.citationKey,revision:n.entry.revision})),x(n.entry),F(n.entry.tags.map(s=>s.id)),M(n.updated?e("citationLibrary.updated"):e("citationLibrary.saved")),C(1),U(s=>s+1)}catch(a){c(k(a)),Ht(n=>n+1)}finally{O(null)}},wt=async()=>{if(y){O(y.citationKey),c("");try{const i=Kt(st,l);if(!i){c(e("citationErrors.invalid"));return}const a=await S(`/api/citations/${encodeURIComponent(y.id)}`,{method:"PATCH",body:JSON.stringify({bibtex:i.bibtex,citationKey:i.citationKey,entryType:i.entryType,title:i.title,authors:i.authors,year:i.year,expectedRevision:y.revision})});N(n=>[a.entry,...n.filter(s=>s.id!==a.entry.id)]),w(n=>{const s=new Map(n);return s.delete(y.citationKey.toLowerCase()),s.set(a.entry.citationKey.toLowerCase(),{id:a.entry.id,citationKey:a.entry.citationKey,revision:a.entry.revision}),s}),T(null),M(e("citationLibrary.updated")),C(1),U(n=>n+1)}catch(i){c(k(i))}finally{O(null)}}},qt=async()=>{if(Q.trim()){O("__new__"),c("");try{const i=Kt(Q,l);if(!i){c(e("citationErrors.invalid"));return}const a=await S("/api/citations",{method:"POST",body:JSON.stringify({bibtex:i.bibtex,citationKey:i.citationKey,entryType:i.entryType,title:i.title,authors:i.authors,year:i.year})});N(n=>[a.entry,...n.filter(s=>s.id!==a.entry.id&&s.citationKey.toLowerCase()!==a.entry.citationKey.toLowerCase())]),w(n=>new Map(n).set(a.entry.citationKey.toLowerCase(),{id:a.entry.id,citationKey:a.entry.citationKey,revision:a.entry.revision})),_(!1),ot(""),M(a.updated?e("citationLibrary.updated"):e("citationLibraryActions.added")),x(a.entry),F(a.entry.tags.map(n=>n.id)),C(1),U(n=>n+1)}catch(i){c(k(i))}finally{O(null)}}},St=async()=>{if(Y.trim()){c("");try{const i=await S("/api/citations/tags",{method:"POST",body:JSON.stringify({name:Y.trim(),color:xt})});pt(a=>[...a.filter(n=>n.id!==i.tag.id),i.tag].sort((n,s)=>n.name.localeCompare(s.name))),gt(""),M(e("citationLibrary.tagCreated"))}catch(i){c(k(i))}}},Vt=i=>{T(null),_(!1),x(i),F(i.tags.map(a=>a.id))},yt=async()=>{if(h){jt(!0),c("");try{const i=await S(`/api/citations/${encodeURIComponent(h.id)}/tags`,{method:"PATCH",body:JSON.stringify({tagIds:B,expectedRevision:h.revision})});N(a=>a.map(n=>n.id===i.entry.id?i.entry:n)),w(a=>new Map(a).set(i.entry.citationKey.toLowerCase(),{id:i.entry.id,citationKey:i.entry.citationKey,revision:i.entry.revision})),x(i.entry),F(i.entry.tags.map(a=>a.id)),M(e("citationLibrary.tagsUpdated")),U(a=>a+1)}catch(i){c(k(i))}finally{jt(!1)}}},Wt=async i=>{ct(i.id),W("");try{await S(`/api/citations/${encodeURIComponent(i.id)}`,{method:"DELETE"}),N(a=>a.filter(n=>n.id!==i.id)),w(a=>{const n=new Map(a);return n.delete(i.citationKey.toLowerCase()),n}),y?.id===i.id&&T(null),h?.id===i.id&&x(null),M(e("citationLibrary.deleted")),C(1),U(a=>a+1),X(null)}catch(a){W(k(a))}finally{ct(null)}},kt=t.jsxs("div",{className:"citation-library-dialog",children:[t.jsxs("div",{className:"citation-library-tabs",role:"tablist","aria-label":e("citationLibrary.title"),children:[t.jsxs("button",{type:"button",role:"tab","aria-selected":K==="library",className:K==="library"?"active":"",onClick:()=>A("library"),children:[t.jsx(tt,{size:15}),e("citationLibrary.myLibrary"),t.jsx("span",{children:g.total})]}),R&&t.jsxs("button",{type:"button",role:"tab","aria-selected":K==="current",className:K==="current"?"active":"",onClick:()=>A("current"),children:[t.jsx(it,{size:15}),e("citationLibrary.currentFile"),t.jsx("span",{children:D.length})]})]}),$&&!(u&&(h||y))&&t.jsx("p",{className:"error dialog-error",children:$}),Lt&&t.jsxs("p",{className:"citation-library-notice",role:"status",children:[t.jsx(J,{size:14}),Lt,t.jsx("button",{type:"button","aria-label":e("common.close"),onClick:()=>M(""),children:t.jsx(et,{size:13})})]}),K==="library"?t.jsxs(t.Fragment,{children:[t.jsxs("div",{className:"citation-library-search-row",children:[t.jsxs("label",{className:"citation-library-search",children:[t.jsx(ii,{size:15}),t.jsx("input",{autoFocus:!0,placeholder:e("citationLibrary.searchPlaceholder"),value:z,onChange:i=>{d(i.target.value),C(1)}}),t.jsx("span",{children:e("citationLibrary.searchHint")})]}),t.jsxs("button",{type:"button",className:"citation-add-entry-button",onClick:()=>{_(!0),T(null),x(null)},children:[t.jsx(it,{size:14}),e("citationLibraryActions.addEntry")]})]}),t.jsxs("div",{className:"citation-tag-bar",children:[t.jsxs("div",{className:"citation-tag-filters",children:[t.jsx("button",{type:"button",className:L?"":"active",onClick:()=>{P(""),C(1)},children:e("citationLibrary.allTags")}),q.map(i=>t.jsxs("button",{type:"button",className:L===i.id?"active":"",onClick:()=>{P(L===i.id?"":i.id),C(1)},children:[t.jsx("span",{className:`citation-tag-dot tag-${i.color}`}),i.name]},i.id)),q.length===0&&t.jsx("span",{className:"muted",children:e("citationLibrary.noTags")})]}),t.jsxs("div",{className:"citation-tag-create",children:[t.jsx("input",{value:Y,placeholder:e("citationLibrary.newTagPlaceholder"),onChange:i=>gt(i.target.value),onKeyDown:i=>{i.key==="Enter"&&(i.preventDefault(),St())}}),t.jsx("select",{"aria-label":e("citationLibrary.tagColor"),value:xt,onChange:i=>_t(i.target.value),children:["red","orange","yellow","green","blue","purple","gray"].map(i=>t.jsx("option",{value:i,children:e(`tags.${i}`)},i))}),t.jsxs("button",{type:"button",disabled:!Y.trim(),title:e("citationLibrary.createTag"),onClick:()=>{St()},children:[t.jsx(it,{size:13}),e("citationLibrary.createTag")]})]})]}),Dt&&t.jsxs("div",{className:"citation-edit-panel",children:[t.jsxs("div",{className:"citation-edit-header",children:[t.jsxs("div",{children:[t.jsx("strong",{children:e("citationLibraryActions.addEntryTitle")}),t.jsx("small",{children:e("citationLibraryActions.addEntryDescription")})]}),t.jsx("button",{type:"button","aria-label":e("common.close"),onClick:()=>_(!1),children:t.jsx(et,{size:15})})]}),t.jsx("textarea",{autoFocus:!0,value:Q,onChange:i=>ot(i.target.value),placeholder:e("citationLibraryActions.addEntryPlaceholder"),spellCheck:!1,rows:9}),t.jsxs("div",{className:"citation-edit-actions",children:[t.jsx("button",{type:"button",onClick:()=>_(!1),children:e("common.cancel")}),t.jsxs("button",{type:"button",className:"primary",disabled:!Q.trim()||V==="__new__",onClick:()=>{qt()},children:[V==="__new__"?t.jsx(H,{className:"spin",size:14}):t.jsx(J,{size:14}),e("citationLibraryActions.addEntry")]})]})]}),Rt?t.jsxs("div",{className:"citation-library-empty",children:[t.jsx(H,{className:"spin",size:22}),t.jsx("span",{children:e("common.loading")})]}):I.length===0?t.jsxs("div",{className:"citation-library-empty",children:[t.jsx(tt,{size:28}),t.jsx("strong",{children:e("citationLibrary.emptyTitle")}),t.jsx("span",{children:e(L?"citationLibrary.noTagMatches":"citationLibrary.emptyDescription")})]}):t.jsx("div",{className:"citation-entry-list",children:I.map(i=>t.jsxs("div",{className:"citation-entry-item",children:[t.jsx(Pt,{entry:i,currentUserId:m,selected:h?.id===i.id||y?.id===i.id,alreadyInFile:!u&&Jt.has(i.citationKey.toLowerCase()),importDisabled:E,deleting:ft===i.id,onImport:f?async()=>{await f(i)&&j(!1)}:void 0,onEdit:i.ownerId===m?()=>{x(null),_(!1),T(i),rt(i.bibtex)}:void 0,onTags:i.ownerId===m?()=>Vt(i):void 0,onDelete:i.ownerId===m?()=>{X(i),W("")}:void 0,t:e}),!u&&h?.id===i.id&&t.jsx(mt,{entry:h,tags:q,selectedIds:B,saving:nt,onClose:()=>x(null),onToggle:a=>F(n=>n.includes(a)?n.filter(s=>s!==a):[...n,a]),onSave:()=>{yt()},t:e}),!u&&y?.id===i.id&&t.jsx(Mt,{title:e("citationLibrary.editTitle",{key:y.citationKey}),value:st,saving:V===y.citationKey,onChange:rt,onClose:()=>T(null),onSave:()=>{wt()},t:e})]},i.id))}),g.totalPages>1&&t.jsxs("nav",{className:"citation-library-pagination","aria-label":e("citationLibraryPagination.pageOf",{page:g.page,totalPages:g.totalPages,count:g.total}),children:[t.jsxs("button",{type:"button",disabled:g.page<=1,onClick:()=>C(i=>Math.max(1,i-1)),children:[t.jsx(bt,{size:14}),e("citationLibraryPagination.previous")]}),t.jsx("span",{children:e("citationLibraryPagination.pageOf",{page:g.page,totalPages:g.totalPages,count:g.total})}),t.jsxs("button",{type:"button",disabled:g.page>=g.totalPages,onClick:()=>C(i=>Math.min(g.totalPages,i+1)),children:[e("citationLibraryPagination.next"),t.jsx(Zt,{size:14})]})]})]}):t.jsxs(t.Fragment,{children:[t.jsxs("div",{className:"citation-current-heading",children:[t.jsxs("div",{children:[t.jsx("strong",{children:e("citationLibrary.currentFileTitle")}),t.jsx("span",{children:e("citationLibrary.currentFileDescription")})]}),t.jsxs("button",{type:"button",onClick:()=>A("library"),children:[t.jsx(bt,{size:14}),e("citationLibrary.backToLibrary")]})]}),Nt&&t.jsxs("p",{className:"warning citation-library-warning",children:[t.jsx(zt,{size:15}),e("citationLibrary.fileTooLarge",{size:Xt(l)})]}),Tt&&t.jsxs("p",{className:"warning citation-library-warning",children:[t.jsx(zt,{size:15}),e("citationLibrary.fileInvalid")]}),D.length===0?t.jsxs("div",{className:"citation-library-empty",children:[t.jsx(tt,{size:28}),t.jsx("strong",{children:e("citationLibrary.noEntries")}),t.jsx("span",{children:e(Nt?"citationLibrary.fileTooLargeHint":Tt?"citationLibrary.fileInvalidHint":"citationLibrary.noEntriesDescription")})]}):t.jsx("div",{className:"citation-entry-list",children:D.map((i,a)=>t.jsxs("div",{className:"citation-entry-item",children:[t.jsx(Pt,{entry:i,saved:Ct.has(i.citationKey.toLowerCase()),saving:V===i.citationKey,saveDisabled:Ot,onSave:()=>{Ut(i)},t:e}),!u&&h?.citationKey.toLowerCase()===i.citationKey.toLowerCase()&&t.jsx(mt,{entry:h,tags:q,selectedIds:B,saving:nt,onClose:()=>x(null),onToggle:n=>F(s=>s.includes(n)?s.filter(Z=>Z!==n):[...s,n]),onSave:()=>{yt()},t:e})]},`${i.citationKey}-${a}`))})]})]}),Et=t.jsx(Bt,{open:!!lt,title:e("citationLibrary.deleteTitle"),description:e("citationLibrary.deleteDescription",{key:lt?.citationKey??""}),confirmLabel:e("common.delete"),danger:!0,error:It,busy:!!ft,onCancel:()=>{X(null),W("")},onConfirm:()=>{const i=lt;i&&Wt(i)}});return u?t.jsxs(t.Fragment,{children:[t.jsxs("section",{className:"citation-library-page",children:[t.jsxs("header",{className:"citation-library-page-header",children:[t.jsxs("div",{children:[t.jsxs("h1",{children:[t.jsx(tt,{"aria-hidden":!0,size:26}),e("citationLibrary.title")]}),t.jsx("p",{children:e("citationLibraryHomeDescription")})]}),b&&t.jsxs("button",{type:"button",onClick:b,children:[t.jsx(bt,{size:15}),e("projects.title")]})]}),kt]}),h&&t.jsx(ut,{open:!0,title:e("citationLibrary.tagsTitle",{key:h.citationKey}),description:e("citationLibrary.tags"),onOpenChange:i=>{i||(x(null),c(""))},children:t.jsxs(t.Fragment,{children:[$&&t.jsx("p",{className:"error dialog-error",children:$}),t.jsx(mt,{entry:h,tags:q,selectedIds:B,saving:nt,modal:!0,onClose:()=>{x(null),c("")},onToggle:i=>F(a=>a.includes(i)?a.filter(n=>n!==i):[...a,i]),onSave:()=>{yt()},t:e})]})}),y&&t.jsx(ut,{open:!0,extraWide:!0,title:e("citationLibrary.editTitle",{key:y.citationKey}),description:e("citationLibrary.title"),onOpenChange:i=>{i||(T(null),c(""))},children:t.jsxs(t.Fragment,{children:[$&&t.jsx("p",{className:"error dialog-error",children:$}),t.jsx(Mt,{title:e("citationLibrary.editTitle",{key:y.citationKey}),value:st,saving:V===y.citationKey,modal:!0,onChange:rt,onClose:()=>{T(null),c("")},onSave:()=>{wt()},t:e})]})}),Et]}):t.jsxs(t.Fragment,{children:[t.jsx(ut,{open:o,extraWide:!0,className:"citation-library-modal",title:e("citationLibrary.title"),description:e(R?"citationLibrary.description":"citationLibraryHomeDescription",{file:v}),onOpenChange:j,footer:t.jsx("button",{onClick:()=>j(!1),children:e("common.close")}),children:kt}),Et]})}function Pt({entry:o,currentUserId:j,selected:u,alreadyInFile:b,saved:v,saving:p,saveDisabled:E,importDisabled:m,deleting:l,onSave:f,onImport:e,onEdit:K,onTags:A,onDelete:z,t:d}){const L="id"in o,P=L?o:null,I=Gt(o);return t.jsxs("article",{className:`citation-entry-card${u?" selected":""}${b?" already-in-file":""}`,title:b?d("citationErrors.alreadyInFile",{key:o.citationKey}):void 0,children:[t.jsxs("div",{className:"citation-entry-main",children:[t.jsxs("div",{className:"citation-entry-heading",children:[t.jsx("code",{children:o.citationKey}),t.jsxs("span",{children:["@",o.entryType]}),I&&t.jsx("span",{className:"citation-entry-venue",title:I,children:I}),v&&t.jsxs("em",{children:[t.jsx(J,{size:11}),d("citationLibrary.savedLabel")]})]}),t.jsx("strong",{children:o.title||d("citationLibrary.untitled")}),t.jsxs("p",{children:[o.authors||d("citationLibrary.authorUnknown"),o.year&&t.jsxs("span",{children:[" · ",o.year]})]}),P&&P.tags.length>0&&t.jsx("div",{className:"citation-entry-tags",children:P.tags.map(N=>t.jsxs("span",{className:`citation-tag-pill tag-${N.color}`,children:[t.jsx("span",{className:"citation-tag-dot"}),N.name]},N.id))})]}),t.jsxs("div",{className:"citation-entry-actions",children:[f&&t.jsxs("button",{type:"button",className:v?"saved":"primary",disabled:p||E,onClick:f,children:[p||E?t.jsx(H,{className:"spin",size:13}):v?t.jsx(J,{size:13}):t.jsx(it,{size:13}),d(p?"citationLibrary.saving":v?"citationLibrary.update":"citationLibrary.save")]}),e&&t.jsxs("button",{type:"button",disabled:m||b,title:b?d("citationErrors.alreadyInFile",{key:o.citationKey}):d(m?"citationLibrary.importRequiresWrite":"citationLibrary.importToFile"),onClick:()=>{e()},children:[t.jsx(ai,{size:13}),d("citationLibrary.import")]}),P&&A&&t.jsxs("button",{type:"button",onClick:A,children:[t.jsx(ei,{size:13}),d("citationLibrary.tags")]}),L&&K&&t.jsxs("button",{type:"button",onClick:K,children:[t.jsx(ni,{size:13}),d("common.edit")]}),L&&z&&t.jsxs("button",{type:"button",className:"danger-text",disabled:l,onClick:z,children:[l?t.jsx(H,{className:"spin",size:13}):t.jsx(ti,{size:13}),d("common.delete")]})]})]})}function mt({entry:o,tags:j,selectedIds:u,saving:b,modal:v=!1,onClose:p,onToggle:E,onSave:m,t:l}){const f=t.jsxs(t.Fragment,{children:[t.jsx("div",{className:"citation-tag-choice-list",children:j.length===0?t.jsx("span",{className:"muted",children:l("citationLibrary.noTagsToAssign")}):j.map(e=>t.jsxs("label",{children:[t.jsx("input",{type:"checkbox",checked:u.includes(e.id),onChange:()=>E(e.id)}),t.jsx("span",{className:`citation-tag-dot tag-${e.color}`}),e.name]},e.id))}),t.jsxs("div",{className:"citation-edit-actions",children:[t.jsx("button",{type:"button",onClick:p,children:l("common.cancel")}),t.jsxs("button",{type:"button",className:"primary",disabled:b||j.length===0,onClick:m,children:[b?t.jsx(H,{className:"spin",size:14}):t.jsx(J,{size:14}),l("citationLibrary.saveTags")]})]})]});return v?t.jsx("div",{className:"citation-modal-form",children:f}):t.jsxs("div",{className:"citation-tag-editor",children:[t.jsxs("div",{className:"citation-edit-header",children:[t.jsx("strong",{children:l("citationLibrary.tagsTitle",{key:o.citationKey})}),t.jsx("button",{type:"button","aria-label":l("common.close"),onClick:p,children:t.jsx(et,{size:15})})]}),f]})}function Mt({title:o,value:j,saving:u,modal:b=!1,onChange:v,onClose:p,onSave:E,t:m}){const l=t.jsxs(t.Fragment,{children:[t.jsx("textarea",{autoFocus:!b,value:j,onChange:f=>v(f.target.value),spellCheck:!1,rows:12}),t.jsxs("div",{className:"citation-edit-actions",children:[t.jsx("button",{type:"button",onClick:p,children:m("common.cancel")}),t.jsxs("button",{type:"button",className:"primary",disabled:!j.trim()||u,onClick:E,children:[u?t.jsx(H,{className:"spin",size:14}):t.jsx(J,{size:14}),m("common.save")]})]})]});return b?t.jsx("div",{className:"citation-modal-form citation-edit-panel",children:l}):t.jsxs("div",{className:"citation-edit-panel",children:[t.jsxs("div",{className:"citation-edit-header",children:[t.jsx("strong",{children:o}),t.jsx("button",{type:"button","aria-label":m("common.close"),onClick:p,children:t.jsx(et,{size:15})})]}),l]})}export{pi as CitationLibraryDialog};