@hosanna/chordpro 1.1.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/LICENSE.md +201 -0
- package/README.md +177 -0
- package/dist/chunk-6VAMM7S3.mjs +388 -0
- package/dist/chunk-CTBAE2XK.mjs +1154 -0
- package/dist/chunk-PENNIH4J.mjs +1143 -0
- package/dist/chunk-QZY7KJWK.mjs +749 -0
- package/dist/chunk-TUKJQWXG.mjs +50 -0
- package/dist/editor/index.d.mts +44 -0
- package/dist/editor/index.d.ts +44 -0
- package/dist/editor/index.js +789 -0
- package/dist/editor/index.mjs +16 -0
- package/dist/index.d.mts +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +3459 -0
- package/dist/index.mjs +54 -0
- package/dist/parser/index.d.mts +185 -0
- package/dist/parser/index.d.ts +185 -0
- package/dist/parser/index.js +1567 -0
- package/dist/parser/index.mjs +32 -0
- package/dist/renderer/index.d.mts +52 -0
- package/dist/renderer/index.d.ts +52 -0
- package/dist/renderer/index.js +2237 -0
- package/dist/renderer/index.mjs +15 -0
- package/package.json +74 -0
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
// src/parser/txt-to-chordpro.ts
|
|
2
|
+
var DEFAULT_OPTIONS = {
|
|
3
|
+
source: "auto",
|
|
4
|
+
detectSections: true,
|
|
5
|
+
strictChordDetection: true,
|
|
6
|
+
dehyphenateSyllables: "auto",
|
|
7
|
+
keepRepeatMarkers: true,
|
|
8
|
+
partTagNames: { start: "start_of_part", end: "end_of_part" }
|
|
9
|
+
};
|
|
10
|
+
var CHORD_ROOT = "(?:[A-G]|D[o\xF3]|R[e\xE9]|Mi|F[a\xE1]|Sol|L[a\xE1]|Si)";
|
|
11
|
+
var CHORD_BODY = CHORD_ROOT + "(?:#|b)?(?:maj|min|m\\(maj7\\)|mM|m7b5|dim|aug|alt|\u0394|\xB0|\xF8|M|m|\\+|-)?(?:2|4|5|6\\/9|6|7|9|11|13)?(?:[Mm+])?(?:sus(?:2|4)?)?(?:add(?:2|4|6|9|11|13))?(?:(?:omit|no)(?:3|5))?(?:\\((?:[#b]?(?:5|9|11|13)|sus(?:2|4)?|add(?:2|4|9|11|13)|alt|omit(?:3|5)|no(?:3|5))(?:[,/]\\s*[#b]?(?:5|9|11|13))*\\))*(?:[#b](?:5|9|11|13))*(?:\\/" + CHORD_ROOT + "(?:#|b)?)?";
|
|
12
|
+
var CHORD_TOKEN_REGEX = new RegExp(`^${CHORD_BODY}$`, "i");
|
|
13
|
+
var NO_CHORD_TOKEN_REGEX = /^N\.?C\.?$/i;
|
|
14
|
+
var CHORD_SCAN_REGEX = new RegExp(
|
|
15
|
+
`(?<=^|\\s)(${CHORD_BODY}|N\\.?C\\.?)(?=\\s|$)`,
|
|
16
|
+
"gi"
|
|
17
|
+
);
|
|
18
|
+
var PUNCTUATION_TOKENS = /* @__PURE__ */ new Set([
|
|
19
|
+
"|",
|
|
20
|
+
"%",
|
|
21
|
+
"-",
|
|
22
|
+
".",
|
|
23
|
+
"...",
|
|
24
|
+
"/",
|
|
25
|
+
"x2",
|
|
26
|
+
"x3",
|
|
27
|
+
"x4",
|
|
28
|
+
"x5",
|
|
29
|
+
"x6",
|
|
30
|
+
"x7",
|
|
31
|
+
"x8",
|
|
32
|
+
"2x",
|
|
33
|
+
"3x",
|
|
34
|
+
"4x",
|
|
35
|
+
"5x",
|
|
36
|
+
"6x",
|
|
37
|
+
"7x",
|
|
38
|
+
"8x"
|
|
39
|
+
]);
|
|
40
|
+
function isValidChordToken(token) {
|
|
41
|
+
const clean = token.replace(/^[(]/, "").replace(/[)]$/, "");
|
|
42
|
+
return CHORD_TOKEN_REGEX.test(clean) || NO_CHORD_TOKEN_REGEX.test(clean);
|
|
43
|
+
}
|
|
44
|
+
function isChordLine(line, strict) {
|
|
45
|
+
const trimmed = line.trim();
|
|
46
|
+
if (!trimmed) return false;
|
|
47
|
+
const tokens = trimmed.split(/\s+/).filter((t) => !PUNCTUATION_TOKENS.has(t.toLowerCase()));
|
|
48
|
+
if (tokens.length === 0) return false;
|
|
49
|
+
const validCount = tokens.filter(isValidChordToken).length;
|
|
50
|
+
return strict ? validCount === tokens.length : validCount / tokens.length >= 0.8;
|
|
51
|
+
}
|
|
52
|
+
function isAlreadyInlineChordPro(line) {
|
|
53
|
+
const matches = [...line.matchAll(/\[([^\]]+)\]/g)];
|
|
54
|
+
if (matches.length === 0) return false;
|
|
55
|
+
return matches.every((m) => isValidChordToken(m[1]));
|
|
56
|
+
}
|
|
57
|
+
var METADATA_MAP = {
|
|
58
|
+
title: "title",
|
|
59
|
+
t: "title",
|
|
60
|
+
titulo: "title",
|
|
61
|
+
t\u00EDtulo: "title",
|
|
62
|
+
subtitle: "subtitle",
|
|
63
|
+
st: "subtitle",
|
|
64
|
+
artist: "artist",
|
|
65
|
+
a: "artist",
|
|
66
|
+
artista: "artist",
|
|
67
|
+
interprete: "artist",
|
|
68
|
+
int\u00E9rprete: "artist",
|
|
69
|
+
composer: "composer",
|
|
70
|
+
music: "composer",
|
|
71
|
+
compositor: "composer",
|
|
72
|
+
lyricist: "lyricist",
|
|
73
|
+
words: "lyricist",
|
|
74
|
+
letra: "lyricist",
|
|
75
|
+
letrista: "lyricist",
|
|
76
|
+
album: "album",
|
|
77
|
+
\u00E1lbum: "album",
|
|
78
|
+
key: "key",
|
|
79
|
+
tom: "key",
|
|
80
|
+
capo: "capo",
|
|
81
|
+
capotraste: "capo",
|
|
82
|
+
capodastro: "capo",
|
|
83
|
+
tuning: "tuning",
|
|
84
|
+
afinacao: "tuning",
|
|
85
|
+
afina\u00E7\u00E3o: "tuning",
|
|
86
|
+
tempo: "tempo",
|
|
87
|
+
andamento: "tempo",
|
|
88
|
+
bpm: "tempo",
|
|
89
|
+
time: "time",
|
|
90
|
+
compasso: "time",
|
|
91
|
+
year: "year",
|
|
92
|
+
ano: "year",
|
|
93
|
+
copyright: "copyright",
|
|
94
|
+
duration: "duration",
|
|
95
|
+
duracao: "duration",
|
|
96
|
+
dura\u00E7\u00E3o: "duration"
|
|
97
|
+
};
|
|
98
|
+
function stripAccents(s) {
|
|
99
|
+
return s.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
|
|
100
|
+
}
|
|
101
|
+
var SECTION_KEYWORDS = {
|
|
102
|
+
verse: ["verso", "verse"],
|
|
103
|
+
chorus: ["refrao", "chorus", "coro"],
|
|
104
|
+
bridge: [
|
|
105
|
+
"ponte",
|
|
106
|
+
"bridge",
|
|
107
|
+
"pre-refrao",
|
|
108
|
+
"prerefrao",
|
|
109
|
+
"prechorus",
|
|
110
|
+
"pre chorus",
|
|
111
|
+
"pre-chorus"
|
|
112
|
+
],
|
|
113
|
+
part: [
|
|
114
|
+
"intro",
|
|
115
|
+
"introducao",
|
|
116
|
+
"introducao1",
|
|
117
|
+
"outro",
|
|
118
|
+
"final",
|
|
119
|
+
"coda",
|
|
120
|
+
"instrumental",
|
|
121
|
+
"interlude",
|
|
122
|
+
"interludio",
|
|
123
|
+
"solo",
|
|
124
|
+
"solo de guitarra",
|
|
125
|
+
"solo de violao",
|
|
126
|
+
"riff"
|
|
127
|
+
]
|
|
128
|
+
};
|
|
129
|
+
var SECTION_TAGS = {
|
|
130
|
+
verse: { start: "start_of_verse", end: "end_of_verse" },
|
|
131
|
+
chorus: { start: "start_of_chorus", end: "end_of_chorus" },
|
|
132
|
+
bridge: { start: "start_of_bridge", end: "end_of_bridge" },
|
|
133
|
+
// 'part' tag names are configurable via options.partTagNames
|
|
134
|
+
part: { start: "start_of_part", end: "end_of_part" }
|
|
135
|
+
};
|
|
136
|
+
var BRACKET_HEADER_REGEX = /^\[([^\]]+)]$/;
|
|
137
|
+
function classifySectionLabel(rawLabel) {
|
|
138
|
+
let s = rawLabel.trim().replace(/:$/, "").trim();
|
|
139
|
+
const base = s.replace(/\s*\d+\s*$/, "").trim();
|
|
140
|
+
const normalized = stripAccents(base).toLowerCase();
|
|
141
|
+
for (const kind of Object.keys(SECTION_KEYWORDS)) {
|
|
142
|
+
if (SECTION_KEYWORDS[kind].some(
|
|
143
|
+
(kw) => normalized === kw || normalized.startsWith(kw)
|
|
144
|
+
)) {
|
|
145
|
+
return { kind, label: s };
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
var REPEAT_NOTATION_REGEX = /\s*\(?\b(\d+)\s*[xX]\b\)?\s*$|\s+[xX](\d+)\s*$/;
|
|
151
|
+
function extractRepeatNotation(line) {
|
|
152
|
+
const match = line.match(REPEAT_NOTATION_REGEX);
|
|
153
|
+
if (match) {
|
|
154
|
+
return {
|
|
155
|
+
cleanLine: line.slice(0, match.index).replace(/\s+$/, ""),
|
|
156
|
+
marker: match[0].trim()
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
return { cleanLine: line, marker: null };
|
|
160
|
+
}
|
|
161
|
+
function countSyllableHyphens(line) {
|
|
162
|
+
const matches = line.match(/[^\s-]-[^\s-]/g);
|
|
163
|
+
return matches ? matches.length : 0;
|
|
164
|
+
}
|
|
165
|
+
function dehyphenate(line) {
|
|
166
|
+
return line.replace(/([^\s-])-([^\s-])/g, "$1 $2");
|
|
167
|
+
}
|
|
168
|
+
function detectSourceFormat(input) {
|
|
169
|
+
const lines = input.split("\n");
|
|
170
|
+
let ugScore = 0;
|
|
171
|
+
let ccScore = 0;
|
|
172
|
+
let sawChordLine = false;
|
|
173
|
+
for (const raw of lines) {
|
|
174
|
+
const line = raw.trim();
|
|
175
|
+
if (!line) continue;
|
|
176
|
+
if (BRACKET_HEADER_REGEX.test(line)) ugScore += 2;
|
|
177
|
+
if (/^(capo|tuning|key)\s*:/i.test(line)) ugScore += 2;
|
|
178
|
+
if (/\s[xX]\d+\s*$/.test(raw)) ugScore += 1;
|
|
179
|
+
if (/^(tom|capotraste|capodastro|int[ée]rprete|compositor|letrista)\s*:/i.test(
|
|
180
|
+
line
|
|
181
|
+
))
|
|
182
|
+
ccScore += 2;
|
|
183
|
+
if (/^\(?\d+x\)?\s*$/i.test(line)) ccScore += 1;
|
|
184
|
+
if (isChordLine(line, true)) {
|
|
185
|
+
sawChordLine = true;
|
|
186
|
+
if (countSyllableHyphens(line) === 0) {
|
|
187
|
+
}
|
|
188
|
+
} else if (countSyllableHyphens(line) >= 2) {
|
|
189
|
+
ccScore += 2;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (!sawChordLine) return "plain";
|
|
193
|
+
if (ccScore > ugScore) return "cifraclub";
|
|
194
|
+
if (ugScore > 0) return "ultimate-guitar";
|
|
195
|
+
return "plain";
|
|
196
|
+
}
|
|
197
|
+
function mergeChordsIntoLyric(chordLine, lyricLine, trailingMarker) {
|
|
198
|
+
const matches = [...chordLine.matchAll(CHORD_SCAN_REGEX)];
|
|
199
|
+
let combined = "";
|
|
200
|
+
let lastIndex = 0;
|
|
201
|
+
for (const match of matches) {
|
|
202
|
+
const token = match[0];
|
|
203
|
+
const index = match.index ?? 0;
|
|
204
|
+
const rendered = NO_CHORD_TOKEN_REGEX.test(token) ? token.toUpperCase() : token;
|
|
205
|
+
combined += lyricLine.substring(lastIndex, index);
|
|
206
|
+
combined += `[${rendered}]`;
|
|
207
|
+
lastIndex = index;
|
|
208
|
+
}
|
|
209
|
+
combined += lyricLine.substring(lastIndex);
|
|
210
|
+
if (trailingMarker) {
|
|
211
|
+
combined += ` ${trailingMarker}`;
|
|
212
|
+
}
|
|
213
|
+
return combined;
|
|
214
|
+
}
|
|
215
|
+
function classify(lines, opts, effectiveSource) {
|
|
216
|
+
const result = [];
|
|
217
|
+
const shouldDehyphenateGlobally = opts.dehyphenateSyllables === true || opts.dehyphenateSyllables === "auto" && effectiveSource === "cifraclub";
|
|
218
|
+
const hyphenThreshold = effectiveSource === "cifraclub" ? 1 : 2;
|
|
219
|
+
for (let i = 0; i < lines.length; i++) {
|
|
220
|
+
const raw = lines[i].replace(/\r$/, "");
|
|
221
|
+
const trimmed = raw.trim();
|
|
222
|
+
if (trimmed === "") {
|
|
223
|
+
result.push({ kind: "blank" });
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
227
|
+
result.push({ kind: "directive", rendered: trimmed });
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (isAlreadyInlineChordPro(trimmed)) {
|
|
231
|
+
result.push({ kind: "inline-chordpro", rendered: raw });
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
const bracketMatch = trimmed.match(BRACKET_HEADER_REGEX);
|
|
235
|
+
if (bracketMatch) {
|
|
236
|
+
const classified = classifySectionLabel(bracketMatch[1]);
|
|
237
|
+
if (classified) {
|
|
238
|
+
result.push({ kind: "section-header", section: classified });
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
const metaMatch = trimmed.match(/^([A-Za-zÀ-ÿ ]+):\s*(.*)/);
|
|
243
|
+
if (metaMatch) {
|
|
244
|
+
const key = stripAccents(metaMatch[1].toLowerCase().trim());
|
|
245
|
+
const value = metaMatch[2];
|
|
246
|
+
if (METADATA_MAP[key]) {
|
|
247
|
+
result.push({
|
|
248
|
+
kind: "metadata",
|
|
249
|
+
rendered: `{${METADATA_MAP[key]}: ${value}}`
|
|
250
|
+
});
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (trimmed.startsWith("#")) {
|
|
255
|
+
result.push({
|
|
256
|
+
kind: "comment",
|
|
257
|
+
rendered: `{comment: ${trimmed.slice(1).trim()}}`
|
|
258
|
+
});
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
if (trimmed.length <= 28 && !isChordLine(trimmed, opts.strictChordDetection)) {
|
|
262
|
+
const classified = classifySectionLabel(trimmed);
|
|
263
|
+
if (classified) {
|
|
264
|
+
result.push({ kind: "section-header", section: classified });
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (isChordLine(trimmed, opts.strictChordDetection)) {
|
|
269
|
+
const nextRaw = i + 1 < lines.length ? lines[i + 1].replace(/\r$/, "") : null;
|
|
270
|
+
const nextIsChord = nextRaw !== null && isChordLine(nextRaw.trim(), opts.strictChordDetection);
|
|
271
|
+
const nextIsBlank = nextRaw !== null && nextRaw.trim() === "";
|
|
272
|
+
if (nextRaw === null || nextIsChord || nextIsBlank) {
|
|
273
|
+
const tokens = trimmed.split(/\s+/);
|
|
274
|
+
result.push({
|
|
275
|
+
kind: "chord-only",
|
|
276
|
+
rendered: tokens.map(
|
|
277
|
+
(t) => NO_CHORD_TOKEN_REGEX.test(t) ? `[${t.toUpperCase()}]` : `[${t}]`
|
|
278
|
+
).join(" ")
|
|
279
|
+
});
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
let lyricLine = nextRaw;
|
|
283
|
+
let marker = null;
|
|
284
|
+
if (opts.keepRepeatMarkers) {
|
|
285
|
+
const extracted = extractRepeatNotation(lyricLine);
|
|
286
|
+
lyricLine = extracted.cleanLine;
|
|
287
|
+
marker = extracted.marker;
|
|
288
|
+
}
|
|
289
|
+
const hyphenCount = countSyllableHyphens(lyricLine);
|
|
290
|
+
if (shouldDehyphenateGlobally && hyphenCount >= hyphenThreshold) {
|
|
291
|
+
lyricLine = dehyphenate(lyricLine);
|
|
292
|
+
} else if (hyphenCount >= 2 && effectiveSource !== "ultimate-guitar") {
|
|
293
|
+
lyricLine = dehyphenate(lyricLine);
|
|
294
|
+
}
|
|
295
|
+
const merged = mergeChordsIntoLyric(raw, lyricLine, marker);
|
|
296
|
+
result.push({ kind: "chord-lyric", rendered: merged });
|
|
297
|
+
i++;
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
result.push({ kind: "lyric", rendered: raw });
|
|
301
|
+
}
|
|
302
|
+
return result;
|
|
303
|
+
}
|
|
304
|
+
function render(lines, opts) {
|
|
305
|
+
const out = [];
|
|
306
|
+
let openSection = null;
|
|
307
|
+
const tagsFor = (kind) => kind === "part" ? opts.partTagNames : SECTION_TAGS[kind];
|
|
308
|
+
const closeSection = () => {
|
|
309
|
+
if (openSection) {
|
|
310
|
+
out.push(`{${tagsFor(openSection.kind).end}}`);
|
|
311
|
+
openSection = null;
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
for (const line of lines) {
|
|
315
|
+
if (line.kind === "section-header" && line.section) {
|
|
316
|
+
if (opts.detectSections) {
|
|
317
|
+
closeSection();
|
|
318
|
+
const tags = tagsFor(line.section.kind);
|
|
319
|
+
out.push(`{${tags.start}: ${line.section.label}}`);
|
|
320
|
+
openSection = line.section;
|
|
321
|
+
} else {
|
|
322
|
+
out.push(`{comment: ${line.section.label}}`);
|
|
323
|
+
}
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
if (line.kind === "blank") {
|
|
327
|
+
if (opts.detectSections) closeSection();
|
|
328
|
+
out.push("");
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
out.push(line.rendered ?? "");
|
|
332
|
+
}
|
|
333
|
+
if (opts.detectSections) closeSection();
|
|
334
|
+
return out.join("\n").replace(/\n{3,}/g, "\n\n").trim() + "\n";
|
|
335
|
+
}
|
|
336
|
+
function extractTitle(chordpro) {
|
|
337
|
+
const match = chordpro.match(/\{title:\s*(.*)\}/i);
|
|
338
|
+
return match ? match[1].trim() : null;
|
|
339
|
+
}
|
|
340
|
+
function slugifyTitle(title) {
|
|
341
|
+
if (!title) return "cifra_convertida";
|
|
342
|
+
return stripAccents(title).trim().replace(/\s+/g, "_").replace(/[^a-zA-Z0-9_]/g, "") || "cifra";
|
|
343
|
+
}
|
|
344
|
+
function convertToChordProDetailed(input, options = {}) {
|
|
345
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
346
|
+
const warnings = [];
|
|
347
|
+
if (!input || !input.trim()) {
|
|
348
|
+
return {
|
|
349
|
+
chordpro: "",
|
|
350
|
+
title: null,
|
|
351
|
+
detectedSource: "plain",
|
|
352
|
+
warnings: ["Empty input."]
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
const detectedSource = opts.source === "auto" ? detectSourceFormat(input) : opts.source;
|
|
356
|
+
const lines = input.split("\n");
|
|
357
|
+
const parsed = classify(lines, opts, detectedSource);
|
|
358
|
+
const chordpro = render(parsed, opts);
|
|
359
|
+
const title = extractTitle(chordpro);
|
|
360
|
+
if (!title)
|
|
361
|
+
warnings.push("No {title: ...} directive was detected in the input.");
|
|
362
|
+
const shorthandTokens = /* @__PURE__ */ new Set();
|
|
363
|
+
for (const raw of lines) {
|
|
364
|
+
if (isChordLine(raw.trim(), opts.strictChordDetection)) {
|
|
365
|
+
for (const m of raw.matchAll(CHORD_SCAN_REGEX)) {
|
|
366
|
+
if (/^[A-G][#b]?4$/i.test(m[0]) || /^[A-G][#b]?9$/i.test(m[0])) {
|
|
367
|
+
shorthandTokens.add(m[0]);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
if (shorthandTokens.size > 0) {
|
|
373
|
+
warnings.push(
|
|
374
|
+
`Ambiguous bare-number chords passed through as-is (not reinterpreted as sus/add): ${[...shorthandTokens].join(", ")}. On Brazilian/Portuguese sheets "G4" usually means Gsus4 - convert manually if your chord engine expects that spelling.`
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
return { chordpro, title, detectedSource, warnings };
|
|
378
|
+
}
|
|
379
|
+
function toChordPro(input, options) {
|
|
380
|
+
return convertToChordProDetailed(input, options).chordpro;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
export {
|
|
384
|
+
detectSourceFormat,
|
|
385
|
+
slugifyTitle,
|
|
386
|
+
convertToChordProDetailed,
|
|
387
|
+
toChordPro
|
|
388
|
+
};
|