@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,749 @@
|
|
|
1
|
+
// src/editor/Editor.tsx
|
|
2
|
+
import React, { Suspense, useCallback, useEffect, useRef, useState } from "react";
|
|
3
|
+
|
|
4
|
+
// src/editor/ChordFinder.ts
|
|
5
|
+
var ChordFinder = class {
|
|
6
|
+
static {
|
|
7
|
+
this.CHORD_REGEX = /\[(.*?)\]/gi;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Extracts chords from text and returns them formatted for Ace Autocomplete
|
|
11
|
+
*/
|
|
12
|
+
static getChords(text) {
|
|
13
|
+
const matches = String(text).match(this.CHORD_REGEX);
|
|
14
|
+
if (!matches || matches.length === 0) {
|
|
15
|
+
return [];
|
|
16
|
+
}
|
|
17
|
+
const chordCounts = matches.reduce(
|
|
18
|
+
(acc, chord) => {
|
|
19
|
+
acc[chord] = (acc[chord] || 0) + 1;
|
|
20
|
+
return acc;
|
|
21
|
+
},
|
|
22
|
+
{}
|
|
23
|
+
);
|
|
24
|
+
return Object.entries(chordCounts).map(([chord, count]) => ({
|
|
25
|
+
value: chord,
|
|
26
|
+
meta: `${count} occurrence${count > 1 ? "s" : ""}`,
|
|
27
|
+
score: count
|
|
28
|
+
// Ace uses score to rank autocomplete suggestions
|
|
29
|
+
})).sort((a, b) => b.score - a.score);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// src/editor/mode-chordpro.ts
|
|
34
|
+
async function registerChordproMode(aceInstance) {
|
|
35
|
+
let ace = aceInstance;
|
|
36
|
+
if (!ace) {
|
|
37
|
+
try {
|
|
38
|
+
const mod = await import("ace-builds");
|
|
39
|
+
ace = mod.default || mod;
|
|
40
|
+
} catch {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (!ace || typeof ace.define !== "function" || ace._chordproModeRegistered) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
ace.define(
|
|
48
|
+
"ace/mode/chordpro_highlight_rules",
|
|
49
|
+
[
|
|
50
|
+
"require",
|
|
51
|
+
"exports",
|
|
52
|
+
"module",
|
|
53
|
+
"ace/lib/oop",
|
|
54
|
+
"ace/mode/text_highlight_rules"
|
|
55
|
+
],
|
|
56
|
+
(require2, exports) => {
|
|
57
|
+
const oop = require2("ace/lib/oop");
|
|
58
|
+
const TextHighlightRules = require2("ace/mode/text_highlight_rules").TextHighlightRules;
|
|
59
|
+
const ChordproHighlightRules = function() {
|
|
60
|
+
const rStart = "(\\{\\s*)";
|
|
61
|
+
const rEnd = "(\\s*\\})";
|
|
62
|
+
const rSep = "(\\s*:\\s*)";
|
|
63
|
+
this.$rules = {
|
|
64
|
+
start: [
|
|
65
|
+
// Comments
|
|
66
|
+
{ token: "comment.line.number-sign", regex: "^#.*$" },
|
|
67
|
+
// Tab Block Start -> moves to tabBlock state
|
|
68
|
+
{
|
|
69
|
+
token: ["punctuation.tag", "markup.bold", "punctuation.tag"],
|
|
70
|
+
regex: rStart + "(start_of_tab|sot)" + rEnd,
|
|
71
|
+
caseInsensitive: true,
|
|
72
|
+
next: "tabBlock"
|
|
73
|
+
},
|
|
74
|
+
// Grid Block Start -> moves to gridBlock state
|
|
75
|
+
{
|
|
76
|
+
token: ["punctuation.tag", "markup.bold", "punctuation.tag"],
|
|
77
|
+
regex: rStart + "(start_of_grid|sog)" + rEnd,
|
|
78
|
+
caseInsensitive: true,
|
|
79
|
+
next: "gridBlock"
|
|
80
|
+
},
|
|
81
|
+
// Generic Block Starts (Chorus, Verse, Bridge, Custom) with optional labels
|
|
82
|
+
{
|
|
83
|
+
token: [
|
|
84
|
+
"punctuation.tag",
|
|
85
|
+
"markup.bold",
|
|
86
|
+
"punctuation.separator",
|
|
87
|
+
"string",
|
|
88
|
+
"punctuation.tag"
|
|
89
|
+
],
|
|
90
|
+
regex: rStart + "(start_of_[a-z_]+|so[a-z])" + rSep + "(.*?)" + rEnd,
|
|
91
|
+
caseInsensitive: true
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
token: ["punctuation.tag", "markup.bold", "punctuation.tag"],
|
|
95
|
+
regex: rStart + "(start_of_[a-z_]+|so[a-z])" + rEnd,
|
|
96
|
+
caseInsensitive: true
|
|
97
|
+
},
|
|
98
|
+
// Generic Block Ends
|
|
99
|
+
{
|
|
100
|
+
token: ["punctuation.tag", "markup.bold", "punctuation.tag"],
|
|
101
|
+
regex: rStart + "(end_of_[a-z_]+|eo[a-z])" + rEnd,
|
|
102
|
+
caseInsensitive: true
|
|
103
|
+
},
|
|
104
|
+
// Formatting & Styling Directives (e.g. textfont, chordsize)
|
|
105
|
+
{
|
|
106
|
+
token: [
|
|
107
|
+
"punctuation.tag",
|
|
108
|
+
"support.type",
|
|
109
|
+
"punctuation.separator",
|
|
110
|
+
"constant.numeric",
|
|
111
|
+
"punctuation.tag"
|
|
112
|
+
],
|
|
113
|
+
regex: rStart + "(textfont|textsize|chordfont|chordsize|tabfont|tabsize|gridfont|gridsize)" + rSep + "(.*?)" + rEnd,
|
|
114
|
+
caseInsensitive: true
|
|
115
|
+
},
|
|
116
|
+
// Meta Directives (e.g. title, key, tempo)
|
|
117
|
+
{
|
|
118
|
+
token: [
|
|
119
|
+
"punctuation.tag",
|
|
120
|
+
"keyword.control",
|
|
121
|
+
"punctuation.separator",
|
|
122
|
+
"string",
|
|
123
|
+
"punctuation.tag"
|
|
124
|
+
],
|
|
125
|
+
regex: rStart + "(title|t|subtitle|st|artist|a|composer|lyricist|ccli|translator|youtube|chorus|copyright|album|year|key|k|time|tempo|duration|capo|meta|c|comment|chord|define|song_number|x_[a-zA-Z0-9_]+)" + rSep + "(.*?)" + rEnd,
|
|
126
|
+
caseInsensitive: true
|
|
127
|
+
},
|
|
128
|
+
// Standalone Directives (e.g. column_break, new_page)
|
|
129
|
+
{
|
|
130
|
+
token: ["punctuation.tag", "keyword.operator", "punctuation.tag"],
|
|
131
|
+
regex: rStart + "(column_break|cb|new_page|np|new_song|ns|chorus)" + rEnd,
|
|
132
|
+
caseInsensitive: true
|
|
133
|
+
},
|
|
134
|
+
// Chord Section barlines (||, |:, :|, |)
|
|
135
|
+
{
|
|
136
|
+
token: "constant.character.barline",
|
|
137
|
+
regex: "\\|\\||:\\||\\|:|\\|"
|
|
138
|
+
},
|
|
139
|
+
// Inline Annotations e.g. [* Bass fill]
|
|
140
|
+
{
|
|
141
|
+
token: ["punctuation.tag", "comment.line", "punctuation.tag"],
|
|
142
|
+
regex: "(\\[\\s*\\*)(.*?)(\\])"
|
|
143
|
+
},
|
|
144
|
+
// Chord with timing annotation e.g. [Em@2x] or [C@0.5x]
|
|
145
|
+
{
|
|
146
|
+
token: [
|
|
147
|
+
"punctuation.tag",
|
|
148
|
+
"constant.language.bold",
|
|
149
|
+
"keyword.operator.timing",
|
|
150
|
+
"punctuation.tag"
|
|
151
|
+
],
|
|
152
|
+
regex: "(\\[)([^\\]@]+)(@[0-9]*\\.?[0-9]+x)(\\])"
|
|
153
|
+
},
|
|
154
|
+
// Chords e.g. [C#maj7/F]
|
|
155
|
+
{
|
|
156
|
+
token: [
|
|
157
|
+
"punctuation.tag",
|
|
158
|
+
"constant.language.bold",
|
|
159
|
+
"punctuation.tag"
|
|
160
|
+
],
|
|
161
|
+
regex: "(\\[)([^\\]]+)(\\])"
|
|
162
|
+
},
|
|
163
|
+
// Invalid/Unknown braces catch-all
|
|
164
|
+
{
|
|
165
|
+
token: ["punctuation.tag", "invalid", "punctuation.tag"],
|
|
166
|
+
regex: rStart + "(.+?)" + rEnd
|
|
167
|
+
}
|
|
168
|
+
],
|
|
169
|
+
// Advanced State for Tablature blocks
|
|
170
|
+
tabBlock: [
|
|
171
|
+
{
|
|
172
|
+
token: ["punctuation.tag", "markup.bold", "punctuation.tag"],
|
|
173
|
+
regex: rStart + "(end_of_tab|eot)" + rEnd,
|
|
174
|
+
caseInsensitive: true,
|
|
175
|
+
next: "start"
|
|
176
|
+
},
|
|
177
|
+
{ token: "comment.line", regex: "-+" },
|
|
178
|
+
// Tab lines
|
|
179
|
+
{ token: "constant.character", regex: "\\|+" },
|
|
180
|
+
// Measure bars
|
|
181
|
+
{ token: "string.regexp", regex: "^[a-gA-G][b#]?\\s*\\|" },
|
|
182
|
+
// Tuning strings at start of line
|
|
183
|
+
{ token: "constant.numeric", regex: "\\b[0-9]+\\b" },
|
|
184
|
+
// Fret numbers
|
|
185
|
+
{ token: "support.function", regex: "[hpsbrv~t]" },
|
|
186
|
+
// Articulations (hammer-on, pull-off, bend)
|
|
187
|
+
{ defaultToken: "comment" }
|
|
188
|
+
// Fallback to comment color for tab lines
|
|
189
|
+
],
|
|
190
|
+
// Advanced State for Grid blocks (Jazz grids / Chord progression boxes)
|
|
191
|
+
gridBlock: [
|
|
192
|
+
{
|
|
193
|
+
token: ["punctuation.tag", "markup.bold", "punctuation.tag"],
|
|
194
|
+
regex: rStart + "(end_of_grid|eog)" + rEnd,
|
|
195
|
+
caseInsensitive: true,
|
|
196
|
+
next: "start"
|
|
197
|
+
},
|
|
198
|
+
{ token: "constant.character", regex: "\\|\\||\\|\\.|\\.\\||\\|" },
|
|
199
|
+
// Grid barlines
|
|
200
|
+
{ token: "keyword.operator", regex: "%" },
|
|
201
|
+
// Grid repeat markers
|
|
202
|
+
{
|
|
203
|
+
token: "constant.language.bold",
|
|
204
|
+
regex: "[A-G][b#]?(?:m|maj|dim|aug|sus|[0-9])*(?:\\/[A-G][b#]?)?"
|
|
205
|
+
},
|
|
206
|
+
// Grid Chords (raw text)
|
|
207
|
+
{ defaultToken: "text" }
|
|
208
|
+
]
|
|
209
|
+
};
|
|
210
|
+
};
|
|
211
|
+
oop.inherits(ChordproHighlightRules, TextHighlightRules);
|
|
212
|
+
exports.ChordproHighlightRules = ChordproHighlightRules;
|
|
213
|
+
}
|
|
214
|
+
);
|
|
215
|
+
ace.define(
|
|
216
|
+
"ace/mode/folding/chordpro",
|
|
217
|
+
[
|
|
218
|
+
"require",
|
|
219
|
+
"exports",
|
|
220
|
+
"module",
|
|
221
|
+
"ace/lib/oop",
|
|
222
|
+
"ace/range",
|
|
223
|
+
"ace/mode/folding/fold_mode"
|
|
224
|
+
],
|
|
225
|
+
(require2, exports) => {
|
|
226
|
+
const oop = require2("ace/lib/oop");
|
|
227
|
+
const Range = require2("ace/range").Range;
|
|
228
|
+
const BaseFoldMode = require2("ace/mode/folding/fold_mode").FoldMode;
|
|
229
|
+
const FoldMode = exports.FoldMode = function() {
|
|
230
|
+
};
|
|
231
|
+
oop.inherits(FoldMode, BaseFoldMode);
|
|
232
|
+
(function() {
|
|
233
|
+
this.foldingStartMarker = /\{\s*(so(?<short>[a-z_]+)|start_of_(?<long>[a-z_]+))(:.*?)?\s*\}/i;
|
|
234
|
+
this.foldingStopMarker = /\{\s*(eo(?<short>[a-z_]+)|end_of_(?<long>[a-z_]+))\s*\}/i;
|
|
235
|
+
const mapBlockName = (name) => {
|
|
236
|
+
const map = {
|
|
237
|
+
c: "chorus",
|
|
238
|
+
v: "verse",
|
|
239
|
+
b: "bridge",
|
|
240
|
+
t: "tab",
|
|
241
|
+
g: "grid"
|
|
242
|
+
};
|
|
243
|
+
return map[name.toLowerCase()] || name.toLowerCase();
|
|
244
|
+
};
|
|
245
|
+
this.getFoldWidgetRange = function(session, _, row) {
|
|
246
|
+
const line = session.getLine(row);
|
|
247
|
+
const match = line.match(this.foldingStartMarker);
|
|
248
|
+
if (match && match.groups) {
|
|
249
|
+
const rawName = match.groups["short"] || match.groups["long"];
|
|
250
|
+
if (!rawName) return;
|
|
251
|
+
return this.getRegionBlock(
|
|
252
|
+
session,
|
|
253
|
+
line,
|
|
254
|
+
row,
|
|
255
|
+
mapBlockName(rawName)
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
this.getRegionBlock = function(session, line, row, normalizedStartName) {
|
|
260
|
+
const startColumn = line.search(/\s*$/);
|
|
261
|
+
const maxRow = session.getLength();
|
|
262
|
+
const startRow = row;
|
|
263
|
+
let hasMatch = false;
|
|
264
|
+
while (++row < maxRow) {
|
|
265
|
+
line = session.getLine(row);
|
|
266
|
+
const m = this.foldingStopMarker.exec(line);
|
|
267
|
+
if (m && m.groups) {
|
|
268
|
+
const rawEndName = m.groups["short"] || m.groups["long"];
|
|
269
|
+
if (rawEndName && mapBlockName(rawEndName) === normalizedStartName) {
|
|
270
|
+
hasMatch = true;
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (hasMatch)
|
|
276
|
+
return new Range(startRow, startColumn, row, line.length);
|
|
277
|
+
};
|
|
278
|
+
}).call(FoldMode.prototype);
|
|
279
|
+
}
|
|
280
|
+
);
|
|
281
|
+
ace.define(
|
|
282
|
+
"ace/mode/chordpro",
|
|
283
|
+
[
|
|
284
|
+
"require",
|
|
285
|
+
"exports",
|
|
286
|
+
"module",
|
|
287
|
+
"ace/lib/oop",
|
|
288
|
+
"ace/mode/text",
|
|
289
|
+
"ace/mode/chordpro_highlight_rules",
|
|
290
|
+
"ace/mode/folding/chordpro"
|
|
291
|
+
],
|
|
292
|
+
(require2, exports) => {
|
|
293
|
+
const oop = require2("ace/lib/oop");
|
|
294
|
+
const TextMode = require2("ace/mode/text").Mode;
|
|
295
|
+
const ChordproHighlightRules = require2("ace/mode/chordpro_highlight_rules").ChordproHighlightRules;
|
|
296
|
+
const FoldMode = require2("ace/mode/folding/chordpro").FoldMode;
|
|
297
|
+
const Mode = function() {
|
|
298
|
+
this.HighlightRules = ChordproHighlightRules;
|
|
299
|
+
this.foldingRules = new FoldMode();
|
|
300
|
+
};
|
|
301
|
+
oop.inherits(Mode, TextMode);
|
|
302
|
+
(function() {
|
|
303
|
+
this.$id = "ace/mode/chordpro";
|
|
304
|
+
this.snippetFileId = "ace/snippets/chordpro";
|
|
305
|
+
}).call(Mode.prototype);
|
|
306
|
+
exports.Mode = Mode;
|
|
307
|
+
}
|
|
308
|
+
);
|
|
309
|
+
ace._chordproModeRegistered = true;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// src/editor/snippets-chordpro.ts
|
|
313
|
+
async function registerChordproSnippets(aceInstance) {
|
|
314
|
+
let ace = aceInstance;
|
|
315
|
+
if (!ace) {
|
|
316
|
+
try {
|
|
317
|
+
const mod = await import("ace-builds");
|
|
318
|
+
ace = mod.default || mod;
|
|
319
|
+
} catch {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
if (!ace || typeof ace.define !== "function" || ace._chordproSnippetsRegistered) {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
ace.define(
|
|
327
|
+
"ace/snippets/chordpro",
|
|
328
|
+
["require", "exports", "module"],
|
|
329
|
+
(_, exports) => {
|
|
330
|
+
exports.snippetText = [
|
|
331
|
+
// album tag
|
|
332
|
+
"snippet album",
|
|
333
|
+
" {album: ${1:value}}",
|
|
334
|
+
"snippet youtube",
|
|
335
|
+
" {youtube: ${1:url}}",
|
|
336
|
+
"snippet number",
|
|
337
|
+
" {song_number: ${1:number}}",
|
|
338
|
+
"snippet cc",
|
|
339
|
+
" {chorus}",
|
|
340
|
+
// arranger tag
|
|
341
|
+
"snippet arranger",
|
|
342
|
+
" {arranger: ${1:value}}",
|
|
343
|
+
// artist tag
|
|
344
|
+
"snippet a",
|
|
345
|
+
" {artist: ${1:value}}",
|
|
346
|
+
"snippet artist",
|
|
347
|
+
" {artist: ${1:value}}",
|
|
348
|
+
// capo tag
|
|
349
|
+
"snippet capo",
|
|
350
|
+
" {capo: ${1:5}}",
|
|
351
|
+
// composer tag
|
|
352
|
+
"snippet composer",
|
|
353
|
+
" {composer: ${1:value}}",
|
|
354
|
+
// copyright tag
|
|
355
|
+
"snippet copyright",
|
|
356
|
+
" {copyright: ${1:value}}",
|
|
357
|
+
// duration tag
|
|
358
|
+
"snippet duration",
|
|
359
|
+
" {duration: ${1:4}:${2:00}}",
|
|
360
|
+
// key tag
|
|
361
|
+
"snippet k",
|
|
362
|
+
" {key: ${1:Am}}",
|
|
363
|
+
"snippet key",
|
|
364
|
+
" {key: ${1:Am}}",
|
|
365
|
+
// lyricist tag
|
|
366
|
+
"snippet lyricist",
|
|
367
|
+
" {lyricist: ${1:value}}",
|
|
368
|
+
// tempo tag
|
|
369
|
+
"snippet tempo",
|
|
370
|
+
" {tempo: ${1:120}}",
|
|
371
|
+
// time tag
|
|
372
|
+
"snippet time",
|
|
373
|
+
" {time: ${1:4}/${2:4}}",
|
|
374
|
+
// title tag
|
|
375
|
+
"snippet t",
|
|
376
|
+
" {title: ${1:value}}",
|
|
377
|
+
"snippet title",
|
|
378
|
+
" {title: ${1:value}}",
|
|
379
|
+
// subtitle tag
|
|
380
|
+
"snippet st",
|
|
381
|
+
" {subtitle: ${1:value}}",
|
|
382
|
+
"snippet subtitle",
|
|
383
|
+
" {subtitle: ${1:value}}",
|
|
384
|
+
// year tag
|
|
385
|
+
"snippet year",
|
|
386
|
+
" {year: ${1:2020}}",
|
|
387
|
+
// meta tag
|
|
388
|
+
"snippet meta",
|
|
389
|
+
" {meta: ${1:label} ${2:value}}",
|
|
390
|
+
// comment tag
|
|
391
|
+
"snippet c",
|
|
392
|
+
" {comment: ${1:value}}",
|
|
393
|
+
"snippet comment",
|
|
394
|
+
" {comment: ${1:value}}",
|
|
395
|
+
// chorus block
|
|
396
|
+
"snippet soc",
|
|
397
|
+
" {start_of_chorus}",
|
|
398
|
+
"snippet eoc",
|
|
399
|
+
" {end_of_chorus}",
|
|
400
|
+
"snippet chorus",
|
|
401
|
+
" {start_of_chorus: ${1:Refr\xE3o}}",
|
|
402
|
+
" ${2:lyrics}",
|
|
403
|
+
" {end_of_chorus}",
|
|
404
|
+
// verse block
|
|
405
|
+
"snippet sov",
|
|
406
|
+
" {start_of_verse}",
|
|
407
|
+
"snippet eov",
|
|
408
|
+
" {end_of_verse}",
|
|
409
|
+
"snippet verse",
|
|
410
|
+
" {start_of_verse: ${1:Verso} ${2:1}}",
|
|
411
|
+
" ${3:lyrics}",
|
|
412
|
+
" {end_of_verse}",
|
|
413
|
+
// bridge block
|
|
414
|
+
"snippet sob",
|
|
415
|
+
" {start_of_bridge}",
|
|
416
|
+
"snippet eob",
|
|
417
|
+
" {end_of_bridge}",
|
|
418
|
+
"snippet bridge",
|
|
419
|
+
" {start_of_bridge: ${1:Ponte}}",
|
|
420
|
+
" ${2:lyrics}",
|
|
421
|
+
" {end_of_bridge}",
|
|
422
|
+
// tabs block
|
|
423
|
+
"snippet sot",
|
|
424
|
+
" {start_of_tab}",
|
|
425
|
+
"snippet eot",
|
|
426
|
+
" {end_of_tab}",
|
|
427
|
+
"snippet tab",
|
|
428
|
+
" {start_of_tab}",
|
|
429
|
+
" e|-${1:-}--------------------------------|",
|
|
430
|
+
" B|----------------------------------|",
|
|
431
|
+
" G|----------------------------------|",
|
|
432
|
+
" D|----------------------------------|",
|
|
433
|
+
" A|----------------------------------|",
|
|
434
|
+
" E|----------------------------------|",
|
|
435
|
+
" {end_of_tab}",
|
|
436
|
+
// define tag
|
|
437
|
+
"snippet d",
|
|
438
|
+
" {define: ${1:Am} base-fret ${2:1} frets ${3:0 0 0 0 0 0} fingers ${4:0 0 0 0 0 0}}",
|
|
439
|
+
"snippet define",
|
|
440
|
+
" {define: ${1:Am} base-fret ${2:1} frets ${3:0 0 0 0 0 0} fingers ${4:0 0 0 0 0 0}}",
|
|
441
|
+
// single-liners
|
|
442
|
+
"snippet cb",
|
|
443
|
+
" {column_break}",
|
|
444
|
+
"snippet column",
|
|
445
|
+
" {column_break}",
|
|
446
|
+
// that's all folks!
|
|
447
|
+
// chord usage
|
|
448
|
+
"snippet [",
|
|
449
|
+
" [${1:Am}]",
|
|
450
|
+
// chord section / grid notation
|
|
451
|
+
"snippet ||",
|
|
452
|
+
" ||[${1:Am}]|[${2:C}]|[${3:G}]|[${4:F}]||",
|
|
453
|
+
"snippet grid",
|
|
454
|
+
" {start_of_grid}",
|
|
455
|
+
" ||[${1:Em}]|[${2:C}]|[${3:D}]||",
|
|
456
|
+
" {end_of_grid}",
|
|
457
|
+
"snippet !",
|
|
458
|
+
" {title: ${1:value}}",
|
|
459
|
+
" {artist: ${2:value}}",
|
|
460
|
+
" {duration: ${3:4:00}}",
|
|
461
|
+
" {key: ${4:C}}",
|
|
462
|
+
" ",
|
|
463
|
+
" ${5:lyrics}",
|
|
464
|
+
" {start_of_chorus}",
|
|
465
|
+
" ${6:lyrics}",
|
|
466
|
+
" {end_of_chorus}"
|
|
467
|
+
].join("\n");
|
|
468
|
+
exports.scope = "chordpro";
|
|
469
|
+
}
|
|
470
|
+
);
|
|
471
|
+
ace._chordproSnippetsRegistered = true;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// src/editor/Editor.tsx
|
|
475
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
476
|
+
var aceLoaderPromise = null;
|
|
477
|
+
function preloadEditor() {
|
|
478
|
+
if (!aceLoaderPromise) {
|
|
479
|
+
aceLoaderPromise = (async () => {
|
|
480
|
+
try {
|
|
481
|
+
const aceModule = await import("ace-builds");
|
|
482
|
+
const ace = aceModule?.default || aceModule;
|
|
483
|
+
if (typeof window !== "undefined" && ace) {
|
|
484
|
+
window.ace = ace;
|
|
485
|
+
}
|
|
486
|
+
const [reactAceModule] = await Promise.all([
|
|
487
|
+
import("react-ace"),
|
|
488
|
+
import("ace-builds/src-noconflict/ext-language_tools"),
|
|
489
|
+
import("ace-builds/src-noconflict/theme-dracula"),
|
|
490
|
+
import("ace-builds/src-noconflict/theme-github"),
|
|
491
|
+
import("ace-builds/src-noconflict/theme-monokai"),
|
|
492
|
+
import("ace-builds/src-noconflict/theme-solarized_dark"),
|
|
493
|
+
import("ace-builds/src-noconflict/theme-solarized_light"),
|
|
494
|
+
import("ace-builds/src-noconflict/theme-textmate"),
|
|
495
|
+
import("ace-builds/src-noconflict/theme-tomorrow"),
|
|
496
|
+
import("ace-builds/src-noconflict/theme-tomorrow_night")
|
|
497
|
+
]);
|
|
498
|
+
const AceEditor = reactAceModule?.default || reactAceModule;
|
|
499
|
+
if (ace) {
|
|
500
|
+
await registerChordproMode(ace);
|
|
501
|
+
await registerChordproSnippets(ace);
|
|
502
|
+
if (typeof ace.require === "function") {
|
|
503
|
+
try {
|
|
504
|
+
const langTools = ace.require("ace/ext/language_tools");
|
|
505
|
+
if (langTools && typeof langTools.addCompleter === "function" && typeof window !== "undefined" && !window._chordproCompleterRegistered) {
|
|
506
|
+
const chordCompleter = {
|
|
507
|
+
getCompletions: (editor, _session, _pos, _prefix, callback) => {
|
|
508
|
+
if (!editor || typeof editor.getValue !== "function") {
|
|
509
|
+
callback(null, []);
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
const text = editor.getValue();
|
|
513
|
+
const chords = ChordFinder.getChords(text);
|
|
514
|
+
callback(null, chords);
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
langTools.addCompleter(chordCompleter);
|
|
518
|
+
window._chordproCompleterRegistered = true;
|
|
519
|
+
}
|
|
520
|
+
} catch {
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
if (!AceEditor) {
|
|
525
|
+
throw new Error("Failed to resolve AceEditor component");
|
|
526
|
+
}
|
|
527
|
+
return AceEditor;
|
|
528
|
+
} catch (error) {
|
|
529
|
+
console.error("Failed to load Ace editor:", error);
|
|
530
|
+
aceLoaderPromise = null;
|
|
531
|
+
const ErrorFallback = () => /* @__PURE__ */ jsx("div", { className: "w-full h-full flex items-center justify-center p-4 text-center text-sm text-red-500 bg-red-50/50 dark:bg-red-950/20 rounded-lg border border-red-200 dark:border-red-900/30", children: "Failed to load code editor. Please ensure 'ace-builds' and 'react-ace' are installed." });
|
|
532
|
+
return ErrorFallback;
|
|
533
|
+
}
|
|
534
|
+
})();
|
|
535
|
+
}
|
|
536
|
+
return aceLoaderPromise;
|
|
537
|
+
}
|
|
538
|
+
var LazyAce = React.lazy(async () => {
|
|
539
|
+
const Component = await preloadEditor();
|
|
540
|
+
return { default: Component };
|
|
541
|
+
});
|
|
542
|
+
var SECTION_LABELS = {
|
|
543
|
+
verse: {
|
|
544
|
+
start: "start_of_verse",
|
|
545
|
+
end: "end_of_verse",
|
|
546
|
+
defaultLabel: "Verso"
|
|
547
|
+
},
|
|
548
|
+
chorus: {
|
|
549
|
+
start: "start_of_chorus",
|
|
550
|
+
end: "end_of_chorus",
|
|
551
|
+
defaultLabel: "Refr\xE3o"
|
|
552
|
+
},
|
|
553
|
+
bridge: {
|
|
554
|
+
start: "start_of_bridge",
|
|
555
|
+
end: "end_of_bridge",
|
|
556
|
+
defaultLabel: "Ponte"
|
|
557
|
+
}
|
|
558
|
+
};
|
|
559
|
+
function wrapSelectionInSection(editor, sectionType) {
|
|
560
|
+
if (!editor || !editor.getSelection || !editor.session) return;
|
|
561
|
+
if (typeof editor.session.getTextRange !== "function" || typeof editor.session.replace !== "function") {
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
const selection = editor.getSelection();
|
|
565
|
+
if (!selection || typeof selection.getRange !== "function") return;
|
|
566
|
+
const range = selection.getRange();
|
|
567
|
+
if (!range) return;
|
|
568
|
+
const selectedText = editor.session.getTextRange(range);
|
|
569
|
+
if (!selectedText || !selectedText.trim()) return;
|
|
570
|
+
const info = SECTION_LABELS[sectionType];
|
|
571
|
+
const wrapped = `{${info.start}: ${info.defaultLabel}}
|
|
572
|
+
${selectedText}
|
|
573
|
+
{${info.end}}`;
|
|
574
|
+
editor.session.replace(range, wrapped);
|
|
575
|
+
}
|
|
576
|
+
var MENU_ITEMS = [
|
|
577
|
+
{ type: "verse", label: "Envolver em Verso", shortcut: "Alt+V" },
|
|
578
|
+
{ type: "chorus", label: "Envolver em Refr\xE3o", shortcut: "Alt+R" },
|
|
579
|
+
{ type: "bridge", label: "Envolver em Ponte", shortcut: "Alt+B" }
|
|
580
|
+
];
|
|
581
|
+
function EditorContextMenu({
|
|
582
|
+
state,
|
|
583
|
+
onAction,
|
|
584
|
+
onClose
|
|
585
|
+
}) {
|
|
586
|
+
const menuRef = useRef(null);
|
|
587
|
+
useEffect(() => {
|
|
588
|
+
const handleClickOutside = (e) => {
|
|
589
|
+
if (menuRef.current && !menuRef.current.contains(e.target)) {
|
|
590
|
+
onClose();
|
|
591
|
+
}
|
|
592
|
+
};
|
|
593
|
+
const handleEscape = (e) => {
|
|
594
|
+
if (e.key === "Escape") onClose();
|
|
595
|
+
};
|
|
596
|
+
if (state.visible) {
|
|
597
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
598
|
+
document.addEventListener("keydown", handleEscape);
|
|
599
|
+
}
|
|
600
|
+
return () => {
|
|
601
|
+
document.removeEventListener("mousedown", handleClickOutside);
|
|
602
|
+
document.removeEventListener("keydown", handleEscape);
|
|
603
|
+
};
|
|
604
|
+
}, [state.visible, onClose]);
|
|
605
|
+
if (!state.visible) return null;
|
|
606
|
+
return /* @__PURE__ */ jsxs(
|
|
607
|
+
"div",
|
|
608
|
+
{
|
|
609
|
+
ref: menuRef,
|
|
610
|
+
className: "fixed z-[9999] min-w-[240px] bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-700 rounded-xl shadow-2xl py-1.5 animate-in fade-in zoom-in-95 duration-100",
|
|
611
|
+
style: { left: state.x, top: state.y },
|
|
612
|
+
children: [
|
|
613
|
+
/* @__PURE__ */ jsx("div", { className: "px-3 py-1.5 text-[10px] font-bold text-zinc-400 dark:text-zinc-500 uppercase tracking-wider select-none border-b border-zinc-100 dark:border-zinc-800 mb-1", children: "Envolver sele\xE7\xE3o em" }),
|
|
614
|
+
MENU_ITEMS.map((item) => /* @__PURE__ */ jsxs(
|
|
615
|
+
"button",
|
|
616
|
+
{
|
|
617
|
+
className: "w-full flex items-center gap-2.5 px-3 py-2 text-left text-sm text-zinc-700 dark:text-zinc-200 hover:bg-indigo-50 dark:hover:bg-indigo-950/40 hover:text-indigo-700 dark:hover:text-indigo-300 transition-colors",
|
|
618
|
+
onClick: () => {
|
|
619
|
+
onAction(item.type);
|
|
620
|
+
onClose();
|
|
621
|
+
},
|
|
622
|
+
children: [
|
|
623
|
+
/* @__PURE__ */ jsx("span", { className: "flex-1 font-medium", children: item.label }),
|
|
624
|
+
/* @__PURE__ */ jsx("span", { className: "text-[10px] font-mono text-zinc-400 dark:text-zinc-500 bg-zinc-100 dark:bg-zinc-800 px-1.5 py-0.5 rounded", children: item.shortcut })
|
|
625
|
+
]
|
|
626
|
+
},
|
|
627
|
+
item.type
|
|
628
|
+
))
|
|
629
|
+
]
|
|
630
|
+
}
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
var DEFAULT_EDITOR_SETTINGS = {
|
|
634
|
+
theme: "textmate",
|
|
635
|
+
fontSize: 14,
|
|
636
|
+
wordWrap: true,
|
|
637
|
+
showLineNumbers: true
|
|
638
|
+
};
|
|
639
|
+
function Editor({
|
|
640
|
+
value,
|
|
641
|
+
onChange,
|
|
642
|
+
onSave,
|
|
643
|
+
settings,
|
|
644
|
+
mode = "chordpro",
|
|
645
|
+
readOnly = false,
|
|
646
|
+
fallback = null
|
|
647
|
+
}) {
|
|
648
|
+
const activeSettings = {
|
|
649
|
+
...DEFAULT_EDITOR_SETTINGS,
|
|
650
|
+
...settings
|
|
651
|
+
};
|
|
652
|
+
const editorRef = useRef(null);
|
|
653
|
+
const [contextMenu, setContextMenu] = useState({
|
|
654
|
+
x: 0,
|
|
655
|
+
y: 0,
|
|
656
|
+
visible: false
|
|
657
|
+
});
|
|
658
|
+
const handleContextMenuAction = useCallback((type) => {
|
|
659
|
+
if (editorRef.current) {
|
|
660
|
+
wrapSelectionInSection(editorRef.current, type);
|
|
661
|
+
}
|
|
662
|
+
}, []);
|
|
663
|
+
const handleLoad = useCallback((editor) => {
|
|
664
|
+
if (!editor) return;
|
|
665
|
+
editorRef.current = editor;
|
|
666
|
+
if (editor.commands && typeof editor.commands.addCommand === "function") {
|
|
667
|
+
editor.commands.addCommand({
|
|
668
|
+
name: "save",
|
|
669
|
+
bindKey: { win: "Ctrl-S", mac: "Cmd-S" },
|
|
670
|
+
exec: (ed) => {
|
|
671
|
+
if (ed && typeof ed.getValue === "function") {
|
|
672
|
+
onSave?.(ed.getValue());
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
});
|
|
676
|
+
editor.commands.addCommand({
|
|
677
|
+
name: "wrapInVerse",
|
|
678
|
+
bindKey: { win: "Alt-V", mac: "Alt-V" },
|
|
679
|
+
exec: (ed) => wrapSelectionInSection(ed, "verse")
|
|
680
|
+
});
|
|
681
|
+
editor.commands.addCommand({
|
|
682
|
+
name: "wrapInChorus",
|
|
683
|
+
bindKey: { win: "Alt-R", mac: "Alt-R" },
|
|
684
|
+
exec: (ed) => wrapSelectionInSection(ed, "chorus")
|
|
685
|
+
});
|
|
686
|
+
editor.commands.addCommand({
|
|
687
|
+
name: "wrapInBridge",
|
|
688
|
+
bindKey: { win: "Alt-B", mac: "Alt-B" },
|
|
689
|
+
exec: (ed) => wrapSelectionInSection(ed, "bridge")
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
if (editor.container && typeof editor.container.addEventListener === "function") {
|
|
693
|
+
const handleContextMenu = (e) => {
|
|
694
|
+
if (!editor || typeof editor.getSelectedText !== "function") return;
|
|
695
|
+
const selectedText = editor.getSelectedText();
|
|
696
|
+
if (selectedText && selectedText.trim()) {
|
|
697
|
+
e.preventDefault();
|
|
698
|
+
e.stopPropagation();
|
|
699
|
+
setContextMenu({ x: e.clientX, y: e.clientY, visible: true });
|
|
700
|
+
}
|
|
701
|
+
};
|
|
702
|
+
editor.container.addEventListener("contextmenu", handleContextMenu);
|
|
703
|
+
}
|
|
704
|
+
}, [onSave]);
|
|
705
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
706
|
+
/* @__PURE__ */ jsx(Suspense, { fallback, children: /* @__PURE__ */ jsx(
|
|
707
|
+
LazyAce,
|
|
708
|
+
{
|
|
709
|
+
mode,
|
|
710
|
+
theme: activeSettings.theme,
|
|
711
|
+
width: "100%",
|
|
712
|
+
height: "100%",
|
|
713
|
+
value,
|
|
714
|
+
onChange,
|
|
715
|
+
onLoad: handleLoad,
|
|
716
|
+
readOnly,
|
|
717
|
+
fontSize: activeSettings.fontSize,
|
|
718
|
+
wrapEnabled: activeSettings.wordWrap,
|
|
719
|
+
showGutter: activeSettings.showLineNumbers,
|
|
720
|
+
setOptions: {
|
|
721
|
+
enableLiveAutocompletion: true,
|
|
722
|
+
enableBasicAutocompletion: true,
|
|
723
|
+
enableSnippets: true,
|
|
724
|
+
showLineNumbers: activeSettings.showLineNumbers,
|
|
725
|
+
tabSize: 2,
|
|
726
|
+
useWorker: false
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
) }),
|
|
730
|
+
/* @__PURE__ */ jsx(
|
|
731
|
+
EditorContextMenu,
|
|
732
|
+
{
|
|
733
|
+
state: contextMenu,
|
|
734
|
+
onAction: handleContextMenuAction,
|
|
735
|
+
onClose: () => setContextMenu((s) => ({ ...s, visible: false }))
|
|
736
|
+
}
|
|
737
|
+
)
|
|
738
|
+
] });
|
|
739
|
+
}
|
|
740
|
+
var Editor_default = Editor;
|
|
741
|
+
|
|
742
|
+
export {
|
|
743
|
+
ChordFinder,
|
|
744
|
+
registerChordproMode,
|
|
745
|
+
registerChordproSnippets,
|
|
746
|
+
preloadEditor,
|
|
747
|
+
Editor,
|
|
748
|
+
Editor_default
|
|
749
|
+
};
|