@retorquere/bibtex-parser 7.0.16 → 8.0.1

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/package.json CHANGED
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "@retorquere/bibtex-parser",
3
- "version": "7.0.16",
3
+ "version": "8.0.1",
4
4
  "description": "BibTeX parser",
5
5
  "main": "index.js",
6
6
  "typings": "./index.d.ts",
7
7
  "scripts": {
8
+ "unified": "tsx unified.ts",
8
9
  "lint": "eslint . --ext .ts --cache --cache-location .eslintcache/",
9
- "prestart": "npm run lint && peggy grammar.peggy",
10
- "start": "tsc -d",
11
- "tsc": "tsc -d",
12
- "poststart": "npm run docs",
10
+ "prestart": "npm run lint",
11
+ "start": "npm run tsc",
12
+ "tsc": "peggy grammar.peggy && tsc -d",
13
13
  "pretest": "npm start",
14
14
  "test": "tap --timeout 0 --disable-coverage --allow-empty-coverage",
15
15
  "snap": "npm test --snapshot",
@@ -18,10 +18,11 @@
18
18
  "list": "tar tvf $(npm pack)",
19
19
  "prepublishOnly": "npm start",
20
20
  "abbr": "./build/abbr.py",
21
- "preversion": "npm test && npm run abbr",
21
+ "preversion": "./console.py r && npm test && npm run abbr",
22
22
  "postversion": "git push --follow-tags",
23
23
  "update-cases": "curl -s https://raw.githubusercontent.com/retorquere/zotero-better-bibtex/master/test/loaded.json | jq -r '.[] | select(test(\"\\\\.bib((la)?tex)?$\"))' | rsync -av --delete -m --files-from=- ../better-bibtex/test/fixtures/ test/better-bibtex/",
24
24
  "x-docs": "rm -rf docs temp && typedoc --name 'BibTeX parser' index.ts && touch docs/.nojekyll",
25
+ "predocs": "npm start",
25
26
  "docs": "mkdir -p etc temp && npm run api-extractor && npm run api-documenter",
26
27
  "ncu": "./build/submodules.py && ./build/crossref.py && ncu -u && npm i && git add package.json package-lock.json && npm audit fix && git commit -m ncu",
27
28
  "api-documenter": "api-documenter markdown --input temp --output docs",
@@ -37,39 +38,47 @@
37
38
  "author": "Emiliano Heyns",
38
39
  "license": "ISC",
39
40
  "devDependencies": {
40
- "@microsoft/api-documenter": "^7.23.31",
41
- "@microsoft/api-extractor": "^7.41.0",
42
- "@types/node": "^20.11.20",
43
- "@typescript-eslint/eslint-plugin": "^7.0.2",
44
- "@typescript-eslint/parser": "^7.0.2",
41
+ "@microsoft/api-documenter": "^7.24.1",
42
+ "@microsoft/api-extractor": "^7.43.0",
43
+ "@types/node": "^20.12.2",
44
+ "@typescript-eslint/eslint-plugin": "^7.4.0",
45
+ "@typescript-eslint/parser": "^7.4.0",
45
46
  "command-line-args": "^5.2.1",
46
- "csv": "^6.3.6",
47
+ "csv": "^6.3.8",
47
48
  "digital-tree": "^2.0.4",
48
49
  "ejs": "^3.1.9",
50
+ "esbuild": "^0.20.2",
49
51
  "eslint": "^8.57.0",
50
52
  "eslint-plugin-import": "^2.29.1",
51
53
  "eslint-plugin-prefer-arrow": "^1.2.3",
52
- "glob": "^10.3.10",
54
+ "glob": "^10.3.12",
53
55
  "mkdirp": "^3.0.1",
54
56
  "node-fetch": "^3.3.2",
55
- "peggy": "^4.0.1",
56
- "piscina": "^4.3.2",
57
- "slug": "^8.2.3",
58
- "tap": "^18.7.0",
57
+ "peggy": "^4.0.2",
58
+ "piscina": "^4.4.0",
59
+ "slug": "^9.0.0",
60
+ "tap": "^18.7.2",
59
61
  "ts-node": "^10.9.2",
60
62
  "tslib": "^2.6.2",
61
- "typescript": "^5.3.3"
63
+ "typescript": "^5.4.3"
62
64
  },
63
65
  "dependencies": {
64
- "unicode2latex": "^5.0.13",
66
+ "@unified-latex/unified-latex-util-pegjs": "^1.7.1",
67
+ "@unified-latex/unified-latex-util-print-raw": "^1.7.1",
68
+ "@unified-latex/unified-latex-util-replace": "^1.7.1",
69
+ "@unified-latex/unified-latex-util-visit": "^1.7.1",
70
+ "compromise": "^14.13.0",
71
+ "unicode2latex": "^6.0.4",
72
+ "wink-eng-lite-web-model": "^1.6.0",
73
+ "wink-nlp": "^2.2.0",
65
74
  "xregexp": "^5.1.1"
66
75
  },
67
76
  "files": [
68
77
  "package.json",
69
78
  "grammar.js",
70
79
  "grammar.d.ts",
71
- "chunker.d.ts",
72
- "chunker.js",
80
+ "verbatim.d.ts",
81
+ "verbatim.js",
73
82
  "index.d.ts",
74
83
  "index.js",
75
84
  "jabref.d.ts",
@@ -1,2 +1,33 @@
1
- export declare function toSentenceCase(sentence: string, preserveQuoted?: boolean): string;
1
+ type WordToken = {
2
+ type: 'word';
3
+ text: string;
4
+ shape: string;
5
+ start: number;
6
+ end: number;
7
+ sentenceStart?: boolean;
8
+ subSentenceStart?: boolean;
9
+ };
10
+ type PunctuationToken = {
11
+ type: 'punctuation';
12
+ text: string;
13
+ shape: string;
14
+ start: number;
15
+ end: number;
16
+ };
17
+ type WhitespaceToken = {
18
+ type: 'whitespace';
19
+ text: string;
20
+ shape: string;
21
+ };
22
+ type Token = WordToken | PunctuationToken | WhitespaceToken;
23
+ export declare function tokenize(title: string, markup?: RegExp): Token[];
24
+ export type Options = {
25
+ preserveQuoted?: boolean;
26
+ subSentenceCapitalization?: boolean;
27
+ markup?: RegExp;
28
+ nocase?: RegExp;
29
+ };
30
+ export declare function toSentenceCase(title: string, options?: Options): string;
31
+ export declare function guessSentenceCased(title: string, markup?: RegExp): boolean;
32
+ export {};
2
33
  //# sourceMappingURL=sentence-case.d.ts.map
package/sentence-case.js CHANGED
@@ -1,90 +1,203 @@
1
1
  "use strict";
2
+ /* eslint-disable @typescript-eslint/unbound-method */
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
2
6
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.toSentenceCase = void 0;
4
- /* eslint-disable @typescript-eslint/no-unsafe-argument */
5
- const categories = require("xregexp/tools/output/categories");
6
- const cat = name => categories.find(c => c.name === name).bmp;
7
- const L = cat('L');
8
- const Lu = cat('Lu');
9
- const Ll = cat('Ll');
10
- const N = cat('N');
11
- const No = cat('No');
12
- const Pc = cat('Pc');
13
- const re = {
14
- acronym: new RegExp(`([${Lu}][.])+$`),
15
- innerCaps: new RegExp(`.[${Lu}]`),
16
- ident: new RegExp(`^[$p{L}]+[${N}${No}][${L}${N}${No}]*$`),
17
- allCaps: new RegExp(`^[${Lu}${N}${No}]+$`),
18
- skipWords: /^(but|or|yet|so|for|and|nor|a|an|the|at|by|from|in|into|of|on|to|with|updown|as)$/i,
19
- // chemElements: /^(H|He|Li|Be|B|C|N|O|F|Ne|Na|Mg|Al|Si|P|S|Cl|Ar|K|Ca|Sc|Ti|V|Cr|Mn|Fe|Co|Ni|Cu|Zn|Ga|Ge|As|Se|Br|Kr|Rb|Sr|Y|Zr|Nb|Mo|Tc|Ru|Rh|Pb|Ag|Cd|In|Sn|Sb|Te|I|Xe|Cs|Ba|La|Hf|Ta|W|Re|Os|Ir|Pt|Au|Hg|Tl|Pb|Bi|Po|At|Rn|Fr|Ra|Ac|Rf|Db|Sg|Bh|Hs|Mt|Ds|Rg|Cn|Nh|Fl|Mc|Lv|Ts|Og|La|Ce|Pr|Nd|Pm|Sm|Eu|Gd|Tb|Dy|Ho|Er|Tm|Yb|Lu|Ac|Th|Pa|U|Np|Pu|Am|Cm|Bk|Cf|Es|Fm|Md|No|Lr)$/,
20
- words: new RegExp(`([\uFFFD${L}${N}${No}]+([\uFFFD${Pc}${L}${N}${No}]*))|(\\s([\uFFFD${Lu}]+[.]){2,})?`, 'g'), // compound words and acronyms
21
- titleCase: new RegExp(`^[${Lu}][${Ll}${N}${No}]+$`),
22
- // private aint = XRegExp("^\\p{L}n't(?=$|[\\P{L}])") // isn't
23
- // private word = XRegExp('^\\p{L}+([-.]\\p{L}+)*') // also match gallium-nitride as one word
24
- // private and = XRegExp('^\\p{Lu}&\\p{Lu}(?=$|[\\P{L}])') // Q&A
25
- subSentenceStart: new RegExp(`([.?!][\\s]+)(<[^>]+>)?([${Lu}])`, 'g'),
26
- sentenceStart: new RegExp(`^(<[^>]+>)?([${Lu}])`, 'g'),
7
+ exports.guessSentenceCased = exports.toSentenceCase = exports.tokenize = void 0;
8
+ const xregexp_1 = __importDefault(require("xregexp"));
9
+ const one_1 = __importDefault(require("compromise/one"));
10
+ const unicode2latex_1 = require("unicode2latex");
11
+ function gather(tokens) {
12
+ return {
13
+ start: tokens[0].start,
14
+ end: tokens[tokens.length - 1].end,
15
+ text: tokens.map(c => c.text).join(''),
16
+ type: 'word',
17
+ shape: tokens.map(c => c.shape).join(''),
18
+ sentenceStart: !!tokens.find(t => t.type === 'word' && t.sentenceStart) || false,
19
+ subSentenceStart: !!tokens.find(t => t.type === 'word' && t.subSentenceStart) || false,
20
+ };
21
+ }
22
+ const preposition = {
23
+ simple: /^(a|about|above|across|after|against|along|among|an|and|around|as|at|before|behind|below|beneath|beside|between|but|by|down|during|for|from|in|inside|into|like|near|nor|of|off|on|onto|or|out|over|so|the|through|to|towards|under|underneath|until|up|upon|with|within|without|yet)$/i,
24
+ complex: /^((according to)|(ahead of)|(apart from)|(as for)|(as of)|(as per)|(as regards)|(aside from)|(back to)|(because of)|(close to)|(due to)|(except for)|(far from)|(inside of)|(instead of)|(next to)|(outside of)|(owing to)|(prior to)|(pursuant to)|(regardless of)|(right of)|(subsequent to))$/i,
27
25
  };
28
- function lowercase(word, allcaps) {
29
- if (!word)
30
- return word;
31
- if (allcaps)
32
- return word.toLowerCase();
33
- const unmasked = word.replace(/\uFFFD/g, '');
34
- if (unmasked.match(re.skipWords))
35
- return word.toLowerCase();
36
- if (unmasked.match(re.titleCase))
37
- return word.toLowerCase();
38
- // if (unmasked.match(re.chemElements)) return word
39
- if (unmasked.length === 1)
40
- return unmasked === 'A' ? word.toLowerCase() : word;
41
- if (unmasked.match(re.innerCaps))
42
- return word;
43
- if (unmasked.match(re.ident) || unmasked.match(re.allCaps))
44
- return word;
45
- return word.toLowerCase();
26
+ function titleCase(s) {
27
+ return s.replace(/^(.)(.+)/, (match, car, cdr) => `${car}${cdr.toLowerCase()}`);
28
+ }
29
+ const Lu = (0, xregexp_1.default)('\\p{Lu}');
30
+ const Ll = (0, xregexp_1.default)('\\p{Ll}');
31
+ const connectedWord = (0, xregexp_1.default)('(^|-)\\p{Lu}\\p{Ll}*(?=-|$)', 'g');
32
+ const connectedInnerWord = (0, xregexp_1.default)('-\\p{Lu}\\p{Ll}*(?=-|$)', 'g');
33
+ const strayCC = new RegExp(`^(${unicode2latex_1.combining.regex})`);
34
+ function wordSC(token, succ, allCaps, subSentence) {
35
+ if (token.type !== 'word')
36
+ return token.text;
37
+ if (token.text.match(/^I'/))
38
+ return titleCase(token.text);
39
+ if (subSentence && token.subSentenceStart && token.text.match(/^a$/i))
40
+ return 'a';
41
+ if ((subSentence && token.subSentenceStart) || token.sentenceStart) {
42
+ return allCaps ? titleCase(token.text) : xregexp_1.default.replace(token.text, connectedInnerWord, match => match.toLowerCase());
43
+ }
44
+ if (!allCaps && token.shape.match(/^[Xxd]+(-[Xxd]+)+/))
45
+ return xregexp_1.default.replace(token.text, connectedWord, match => match.toLowerCase());
46
+ if (token.text.match(/^[B-Z]$/))
47
+ return token.text;
48
+ if (!allCaps && token.shape.match(/^[-X]+$/))
49
+ return token.text;
50
+ if (succ && `${token.text} ${succ.text}`.match(preposition.complex)) {
51
+ succ.text = succ.text.toLowerCase();
52
+ return token.text.toLowerCase();
53
+ }
54
+ if (token.text.match(preposition.simple))
55
+ return token.text.toLowerCase();
56
+ const shape = token.shape.replace(/[^Xxd]/g, '');
57
+ if (!allCaps && shape.match(/^[Xd-]+$/))
58
+ return token.text;
59
+ if (shape.match(/^X[xd]*(-[Xxd]*)*$/))
60
+ return token.text.toLowerCase();
61
+ if (shape.match(/^[Xd]+$/))
62
+ return allCaps ? token.text.toLowerCase() : token.text;
63
+ if (token.text.includes('.'))
64
+ return token.text;
65
+ if (shape.match(/x.*X/))
66
+ return token.text;
67
+ return token.text.toLowerCase();
68
+ }
69
+ function tokentype(token) {
70
+ if (token.type === 'word' && token.text.match(/^\u2060+$/))
71
+ return 'm';
72
+ if (token.type === 'word')
73
+ return 'w';
74
+ if (token.shape === '-')
75
+ return '-';
76
+ return ' ';
46
77
  }
47
- function toSentenceCase(sentence, preserveQuoted = false) {
48
- const allcaps = sentence === sentence.toUpperCase();
49
- const preserve = [];
50
- sentence.replace(re.subSentenceStart, (match, end, markup, char, i) => {
51
- if (!sentence.substring(0, i + 1).match(re.acronym)) {
52
- preserve.push({ pos: i + end.length + ((markup === null || markup === void 0 ? void 0 : markup.length) || 0), text: char, description: 'sub-sentence-start' });
78
+ function prepost(s, offset) {
79
+ const tokens = [];
80
+ for (const text of s.match(/(\s+|\S+)/g)) {
81
+ if (text.match(/\s/)) {
82
+ tokens.push({ type: 'whitespace', text, shape: text });
53
83
  }
54
- return '';
55
- });
56
- sentence.replace(re.sentenceStart, (match, markup, char) => {
57
- preserve.push({ pos: ((markup === null || markup === void 0 ? void 0 : markup.length) || 0), text: char, description: 'sentence-start' });
58
- return '';
59
- });
60
- sentence.replace(/<span class="nocase">.*?<\/span>|<nc>.*?<\/nc>/gi, (text, pos) => {
61
- preserve.push({ pos, text, description: 'nocase' });
62
- return '';
63
- });
64
- let masked = sentence.replace(/<[^>]+>/g, (text, pos) => {
65
- preserve.push({ pos, text, description: 'markup' });
66
- return '\uFFFD'.repeat(text.length);
67
- });
68
- // last because we're potentially modifying the original
69
- for (const q of [/(“)(.*?)”/g, /(‘)(.*?)’/g, /(["])(.*?)\1/g]) {
70
- sentence.replace(q, (text, quote, quoted, pos) => {
71
- preserve.push({
72
- pos: pos + (preserveQuoted ? 0 : quote.length),
73
- text: preserveQuoted ? text : toSentenceCase(quoted, preserveQuoted),
74
- description: `quoted with ${q}`,
84
+ else if (text) {
85
+ tokens.push({ type: 'punctuation', text, shape: text, start: offset, end: offset + text.length - 1 });
86
+ }
87
+ offset += text.length;
88
+ }
89
+ return tokens;
90
+ }
91
+ function tokenize(title, markup) {
92
+ if (markup)
93
+ title = title.replace(markup, match => '\u2060'.repeat(match.length));
94
+ const doc = (0, one_1.default)(title);
95
+ const tokens = [];
96
+ for (const sentence of doc.json({ offset: true })) {
97
+ const words = [];
98
+ let sentenceStart = true;
99
+ let subSentenceStart = false;
100
+ for (const term of sentence.terms) {
101
+ const m = term.post.match(strayCC);
102
+ if (m) {
103
+ term.text += m[0];
104
+ term.post = term.post.substring(m[0].length);
105
+ term.offset.length += m[0].length;
106
+ }
107
+ if (term.pre)
108
+ words.push(...prepost(term.pre, term.offset.start - term.pre.length));
109
+ words.push({
110
+ start: term.offset.start,
111
+ end: term.offset.start + term.offset.length - 1,
112
+ text: term.text,
113
+ type: 'word',
114
+ shape: xregexp_1.default.replaceEach(term.text, [[Lu, 'X', 'all'], [Ll, 'x', 'all'], [/\d/g, 'd']]),
115
+ sentenceStart,
116
+ subSentenceStart,
75
117
  });
76
- return '';
77
- });
118
+ sentenceStart = false;
119
+ subSentenceStart = term.post.includes(':');
120
+ if (term.post)
121
+ words.push(...prepost(term.post, term.offset.start + term.offset.length));
122
+ }
123
+ subSentenceStart = true;
124
+ while (words.length) {
125
+ if (words[0].type === 'word' && words[0].text.includes('/')) {
126
+ const multiple = words.shift();
127
+ let start = multiple.start;
128
+ words.unshift(...multiple.text.match(/([/]|[^/]+)/g).map((w) => {
129
+ const started = start;
130
+ start += w.length;
131
+ const ss = w === '/' ? [false, false] : [multiple.sentenceStart, multiple.subSentenceStart];
132
+ if (w !== '/')
133
+ multiple.sentenceStart = multiple.subSentenceStart = false;
134
+ return {
135
+ start: started,
136
+ end: start - 1,
137
+ text: w,
138
+ type: w === '/' ? 'punctuation' : 'word',
139
+ shape: xregexp_1.default.replaceEach(w, [[Lu, 'X', 'all'], [Ll, 'x', 'all'], [/\d/g, 'd']]),
140
+ sentenceStart: ss[0],
141
+ subSentenceStart: ss[1],
142
+ };
143
+ }));
144
+ continue;
145
+ }
146
+ const type = words.map(tokentype).join('');
147
+ let m;
148
+ if (m = type.match(/^m?[wc]m?(-m?[wc]m?)+/)) {
149
+ tokens.push(gather(words.splice(0, m[0].length)));
150
+ continue;
151
+ }
152
+ const word = words.shift();
153
+ if (word.type !== 'whitespace')
154
+ tokens.push(word);
155
+ }
78
156
  }
79
- masked = masked
80
- .replace(/[;:]\uFFFD*\s+\uFFFD*A\s/g, match => match.toLowerCase())
81
- .replace(/[–—]\uFFFD*\s*\uFFFD*A\s/g, match => match.toLowerCase())
82
- .replace(re.words, word => lowercase(word, allcaps));
83
- sentence = masked;
84
- for (const { pos, text } of preserve) {
85
- sentence = sentence.substring(0, pos) + text + sentence.substring(pos + text.length);
157
+ return tokens;
158
+ }
159
+ exports.tokenize = tokenize;
160
+ function toSentenceCase(title, options = {}) {
161
+ options = Object.assign({ preserveQuoted: true, subSentenceCapitalization: true }, options);
162
+ title = title.normalize('NFC'); // https://github.com/winkjs/wink-nlp/issues/134
163
+ const allCaps = title === title.toUpperCase();
164
+ if (allCaps && !title.match(/\s/))
165
+ return title;
166
+ let sentenceCased = title;
167
+ const tokens = tokenize(title, options.markup);
168
+ tokens.forEach((token, i) => {
169
+ if (token.type !== 'whitespace') {
170
+ sentenceCased = sentenceCased.substring(0, token.start) + wordSC(token, tokens[i + 1], allCaps, options.subSentenceCapitalization) + sentenceCased.substring(token.end + 1);
171
+ }
172
+ });
173
+ if (options.markup) {
174
+ for (const match of title.matchAll(options.markup)) {
175
+ sentenceCased = sentenceCased.substring(0, match.index) + match[0] + sentenceCased.substring(match.index + match[0].length);
176
+ }
177
+ }
178
+ if (options.nocase) {
179
+ sentenceCased = sentenceCased
180
+ .replace(options.nocase, (match, tag, offset) => title.substring(offset, offset + match.length));
86
181
  }
87
- return sentence;
182
+ if (options.preserveQuoted) {
183
+ for (const q of [/“.*?”/g, /‘.*?’/g, /".*?"/g]) {
184
+ sentenceCased = sentenceCased.replace(q, (match, offset) => title.substring(offset, offset + match.length));
185
+ }
186
+ }
187
+ return sentenceCased;
88
188
  }
89
189
  exports.toSentenceCase = toSentenceCase;
190
+ function guessSentenceCased(title, markup = /<\/?(?:i|b|sup|sub|ncx?)>/g) {
191
+ const noMarkup = title.replace(markup, '');
192
+ if (noMarkup === noMarkup.toUpperCase())
193
+ return false;
194
+ if (noMarkup === noMarkup.toLowerCase())
195
+ return false;
196
+ const words = tokenize(title, markup).filter(token => token.type === 'word' && token.shape.match(/x/i));
197
+ if (!words.length)
198
+ return true;
199
+ const titleCased = words.filter(word => word.shape.match(/^X+$|^X.*x|[^Xx]+-X/));
200
+ return (titleCased.length / words.length) < 0.5; // eslint-disable-line no-magic-numbers
201
+ }
202
+ exports.guessSentenceCased = guessSentenceCased;
90
203
  //# sourceMappingURL=sentence-case.js.map