linked-rolls 0.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/README.md +18 -0
- package/lib/Collation.d.ts +4 -0
- package/lib/Collation.js +1 -0
- package/lib/Condition.d.ts +10 -0
- package/lib/Condition.js +1 -0
- package/lib/ConditionState.d.ts +9 -0
- package/lib/ConditionState.js +6 -0
- package/lib/Edit.d.ts +21 -0
- package/lib/Edit.js +112 -0
- package/lib/Edition.d.ts +54 -0
- package/lib/Edition.js +1 -0
- package/lib/EditorialAssumption.d.ts +67 -0
- package/lib/EditorialAssumption.js +26 -0
- package/lib/Emulation.d.ts +76 -0
- package/lib/Emulation.js +474 -0
- package/lib/Feature.d.ts +37 -0
- package/lib/Feature.js +3 -0
- package/lib/Measurement.d.ts +9 -0
- package/lib/Measurement.js +1 -0
- package/lib/PlaceTimeConversion.d.ts +65 -0
- package/lib/PlaceTimeConversion.js +175 -0
- package/lib/RollCopy.d.ts +85 -0
- package/lib/RollCopy.js +273 -0
- package/lib/RollEvent.d.ts +76 -0
- package/lib/RollEvent.js +3 -0
- package/lib/Stage.d.ts +37 -0
- package/lib/Stage.js +165 -0
- package/lib/Symbol.d.ts +62 -0
- package/lib/Symbol.js +29 -0
- package/lib/TrackerBar.d.ts +8 -0
- package/lib/TrackerBar.js +53 -0
- package/lib/Transcription.d.ts +7 -0
- package/lib/Transcription.js +9 -0
- package/lib/Version.d.ts +42 -0
- package/lib/Version.js +217 -0
- package/lib/WithId.d.ts +3 -0
- package/lib/WithId.js +1 -0
- package/lib/alignFeatures.d.ts +11 -0
- package/lib/alignFeatures.js +54 -0
- package/lib/alignRolls.d.ts +7 -0
- package/lib/alignRolls.js +49 -0
- package/lib/alignSymbols.d.ts +7 -0
- package/lib/alignSymbols.js +49 -0
- package/lib/asJsonLd.d.ts +3 -0
- package/lib/asJsonLd.js +67 -0
- package/lib/asMIDISpans.d.ts +23 -0
- package/lib/asMIDISpans.js +111 -0
- package/lib/aton/AtonParser.d.ts +48 -0
- package/lib/aton/AtonParser.js +245 -0
- package/lib/aton/AtonParser.test.d.ts +1 -0
- package/lib/aton/AtonParser.test.js +16 -0
- package/lib/build-schema.cjs +113 -0
- package/lib/context.d.ts +1 -0
- package/lib/context.js +1 -0
- package/lib/importJsonLd.d.ts +4 -0
- package/lib/importJsonLd.js +121 -0
- package/lib/index.d.ts +17 -0
- package/lib/index.js +17 -0
- package/lib/spec/context.json +192 -0
- package/package.json +27 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { dimensionOf } from "./Symbol";
|
|
2
|
+
export function alignSymbols(rollA, rollB) {
|
|
3
|
+
// 1. Only musical-note anchors
|
|
4
|
+
const XA = rollA.filter(e => e.type === 'note').map(e => dimensionOf(e).horizontal.from);
|
|
5
|
+
const YB = rollB.filter(e => e.type === 'note').map(e => dimensionOf(e).horizontal.from);
|
|
6
|
+
// 2. Initial LSQ fit for x2 = A*x + B
|
|
7
|
+
let { A, B } = fitAffine(XA, YB);
|
|
8
|
+
let stretch = A, shift = B / A;
|
|
9
|
+
// 3. Refinement
|
|
10
|
+
for (let i = 0; i < 10; i++) {
|
|
11
|
+
const transformed = XA.map(x => stretch * (x + shift));
|
|
12
|
+
const pairs = [];
|
|
13
|
+
for (let tx of transformed) {
|
|
14
|
+
const nearest = findClosest(tx, YB);
|
|
15
|
+
if (Math.abs(tx - nearest) <= 3) {
|
|
16
|
+
pairs.push([(tx / stretch) - shift, nearest]);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
if (pairs.length < 2)
|
|
20
|
+
break;
|
|
21
|
+
const [Xin, Yin] = unzip(pairs);
|
|
22
|
+
const { A: A2, B: B2 } = fitAffine(Xin, Yin);
|
|
23
|
+
const b2 = B2 / A2;
|
|
24
|
+
if (Math.abs(A2 - A) < 1e-6 && Math.abs(b2 - shift) < 1e-3)
|
|
25
|
+
break;
|
|
26
|
+
A = A2;
|
|
27
|
+
B = B2;
|
|
28
|
+
stretch = A;
|
|
29
|
+
shift = b2;
|
|
30
|
+
}
|
|
31
|
+
return { stretch: stretch, shift: shift };
|
|
32
|
+
}
|
|
33
|
+
const mean = (arr) => arr.reduce((acc, val) => acc + val, 0) / arr.length;
|
|
34
|
+
function fitAffine(X, Y) {
|
|
35
|
+
const mx = mean(X), my = mean(Y);
|
|
36
|
+
let num = 0, den = 0;
|
|
37
|
+
X.forEach((x, i) => { num += (x - mx) * (Y[i] - my); den += (x - mx) ** 2; });
|
|
38
|
+
const A = den === 0 ? 1 : num / den;
|
|
39
|
+
const B = my - A * mx;
|
|
40
|
+
return { A, B };
|
|
41
|
+
}
|
|
42
|
+
function findClosest(val, arr) {
|
|
43
|
+
return arr.reduce((best, curr) => Math.abs(curr - val) < Math.abs(best - val) ? curr : best);
|
|
44
|
+
}
|
|
45
|
+
function unzip(p) {
|
|
46
|
+
const X = [], Y = [];
|
|
47
|
+
p.forEach(([x, y]) => { X.push(x); Y.push(y); });
|
|
48
|
+
return [X, Y];
|
|
49
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { dimensionOf } from "./Symbol";
|
|
2
|
+
export function alignSymbols(rollA, rollB) {
|
|
3
|
+
// 1. Only musical-note anchors
|
|
4
|
+
const XA = rollA.filter(e => e.type === 'note').map(e => dimensionOf(e).horizontal.from);
|
|
5
|
+
const YB = rollB.filter(e => e.type === 'note').map(e => dimensionOf(e).horizontal.from);
|
|
6
|
+
// 2. Initial LSQ fit for x2 = A*x + B
|
|
7
|
+
let { A, B } = fitAffine(XA, YB);
|
|
8
|
+
let stretch = A, shift = B / A;
|
|
9
|
+
// 3. Refinement
|
|
10
|
+
for (let i = 0; i < 10; i++) {
|
|
11
|
+
const transformed = XA.map(x => stretch * (x + shift));
|
|
12
|
+
const pairs = [];
|
|
13
|
+
for (let tx of transformed) {
|
|
14
|
+
const nearest = findClosest(tx, YB);
|
|
15
|
+
if (Math.abs(tx - nearest) <= 3) {
|
|
16
|
+
pairs.push([(tx / stretch) - shift, nearest]);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
if (pairs.length < 2)
|
|
20
|
+
break;
|
|
21
|
+
const [Xin, Yin] = unzip(pairs);
|
|
22
|
+
const { A: A2, B: B2 } = fitAffine(Xin, Yin);
|
|
23
|
+
const b2 = B2 / A2;
|
|
24
|
+
if (Math.abs(A2 - A) < 1e-6 && Math.abs(b2 - shift) < 1e-3)
|
|
25
|
+
break;
|
|
26
|
+
A = A2;
|
|
27
|
+
B = B2;
|
|
28
|
+
stretch = A;
|
|
29
|
+
shift = b2;
|
|
30
|
+
}
|
|
31
|
+
return { stretch: stretch, shift: shift };
|
|
32
|
+
}
|
|
33
|
+
const mean = (arr) => arr.reduce((acc, val) => acc + val, 0) / arr.length;
|
|
34
|
+
function fitAffine(X, Y) {
|
|
35
|
+
const mx = mean(X), my = mean(Y);
|
|
36
|
+
let num = 0, den = 0;
|
|
37
|
+
X.forEach((x, i) => { num += (x - mx) * (Y[i] - my); den += (x - mx) ** 2; });
|
|
38
|
+
const A = den === 0 ? 1 : num / den;
|
|
39
|
+
const B = my - A * mx;
|
|
40
|
+
return { A, B };
|
|
41
|
+
}
|
|
42
|
+
function findClosest(val, arr) {
|
|
43
|
+
return arr.reduce((best, curr) => Math.abs(curr - val) < Math.abs(best - val) ? curr : best);
|
|
44
|
+
}
|
|
45
|
+
function unzip(p) {
|
|
46
|
+
const X = [], Y = [];
|
|
47
|
+
p.forEach(([x, y]) => { X.push(x); Y.push(y); });
|
|
48
|
+
return [X, Y];
|
|
49
|
+
}
|
package/lib/asJsonLd.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { exportDate } from "./importJsonLd";
|
|
2
|
+
// for these keys, references by id will be inserted
|
|
3
|
+
// rather than the object itself.
|
|
4
|
+
export const referenceTypes = [
|
|
5
|
+
'premises',
|
|
6
|
+
'delete',
|
|
7
|
+
'comprehends'
|
|
8
|
+
];
|
|
9
|
+
const asIDArray = (arr) => {
|
|
10
|
+
return arr.map(e => e.id);
|
|
11
|
+
};
|
|
12
|
+
const asJsonLdEntity = (obj) => {
|
|
13
|
+
if (obj instanceof Date) {
|
|
14
|
+
return exportDate(obj);
|
|
15
|
+
}
|
|
16
|
+
const result = {};
|
|
17
|
+
if ('asJSON' in obj && typeof obj['asJSON'] === 'function') {
|
|
18
|
+
return asJsonLdEntity(obj['asJSON']());
|
|
19
|
+
}
|
|
20
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
21
|
+
if (typeof value === 'function' || typeof value === 'undefined') {
|
|
22
|
+
// ignore
|
|
23
|
+
}
|
|
24
|
+
else if (referenceTypes.includes(key)) {
|
|
25
|
+
if (!Array.isArray(value)) {
|
|
26
|
+
console.error(`Expected array for key ${key}, got ${value}`);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
result[key] = asIDArray(value);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
else if (key === 'assigned'
|
|
33
|
+
&& (result['@type'] === 'derivation'
|
|
34
|
+
|| result['@type'] === 'carrierAssignment')) {
|
|
35
|
+
result['assigned'] = value.id;
|
|
36
|
+
}
|
|
37
|
+
else if (key === 'type') {
|
|
38
|
+
result['@type'] = value;
|
|
39
|
+
}
|
|
40
|
+
else if (key === 'id') {
|
|
41
|
+
result['@id'] = value;
|
|
42
|
+
}
|
|
43
|
+
else if (Array.isArray(value)) {
|
|
44
|
+
result[key] = value.map(v => (typeof v === 'object') ? asJsonLdEntity(v) : v);
|
|
45
|
+
}
|
|
46
|
+
else if (typeof value === 'object') {
|
|
47
|
+
result[key] = asJsonLdEntity(value);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
result[key] = value;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return result;
|
|
54
|
+
};
|
|
55
|
+
export const asJsonLd = (edition) => {
|
|
56
|
+
const result = {
|
|
57
|
+
'@context': [
|
|
58
|
+
'https://linked-rolls.org/rollo/1.0/edition.jsonld',
|
|
59
|
+
{
|
|
60
|
+
'@base': edition.base
|
|
61
|
+
}
|
|
62
|
+
],
|
|
63
|
+
'@type': "Edition",
|
|
64
|
+
...asJsonLdEntity(edition)
|
|
65
|
+
};
|
|
66
|
+
return result;
|
|
67
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { MidiFile } from "midifile-ts";
|
|
2
|
+
export declare function midiTickToMilliseconds(ticks: number, microsecondsPerBeat: number, ppq: number): number;
|
|
3
|
+
interface Span<T extends string> {
|
|
4
|
+
type: T;
|
|
5
|
+
id: string;
|
|
6
|
+
onset: number;
|
|
7
|
+
offset: number;
|
|
8
|
+
onsetMs: number;
|
|
9
|
+
offsetMs: number;
|
|
10
|
+
link?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface NoteSpan extends Span<'note'> {
|
|
13
|
+
pitch: number;
|
|
14
|
+
velocity: number;
|
|
15
|
+
channel: number;
|
|
16
|
+
}
|
|
17
|
+
export interface SustainSpan extends Span<'sustain'> {
|
|
18
|
+
}
|
|
19
|
+
export interface SoftSpan extends Span<'soft'> {
|
|
20
|
+
}
|
|
21
|
+
export type AnySpan = NoteSpan | SustainSpan | SoftSpan;
|
|
22
|
+
export declare const asSpans: (file: MidiFile, readLinks?: boolean) => AnySpan[];
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { MIDIControlEvents } from "midifile-ts";
|
|
2
|
+
const isNoteOn = (event) => event.type === 'channel' && event.subtype === 'noteOn';
|
|
3
|
+
const isNoteOff = (event) => event.type === 'channel' && event.subtype === 'noteOff';
|
|
4
|
+
const isPedalOn = (event) => (event.type === 'channel'
|
|
5
|
+
&& event.subtype === 'controller'
|
|
6
|
+
&& event.controllerType === MIDIControlEvents.SUSTAIN
|
|
7
|
+
&& event.value > 63);
|
|
8
|
+
const isPedalOff = (event) => (event.type === 'channel'
|
|
9
|
+
&& event.subtype === 'controller'
|
|
10
|
+
&& event.controllerType === MIDIControlEvents.SUSTAIN
|
|
11
|
+
&& event.value <= 63);
|
|
12
|
+
const isSoftPedalOn = (event) => {
|
|
13
|
+
return event.type === 'channel'
|
|
14
|
+
&& event.subtype === 'controller'
|
|
15
|
+
&& event.controllerType === MIDIControlEvents.SOFT_PEDAL
|
|
16
|
+
&& event.value > 63;
|
|
17
|
+
};
|
|
18
|
+
const isSoftPedalOff = (event) => {
|
|
19
|
+
return event.type === 'channel'
|
|
20
|
+
&& event.subtype === 'controller'
|
|
21
|
+
&& event.controllerType === MIDIControlEvents.SOFT_PEDAL
|
|
22
|
+
&& event.value <= 63;
|
|
23
|
+
};
|
|
24
|
+
export function midiTickToMilliseconds(ticks, microsecondsPerBeat, ppq) {
|
|
25
|
+
const beats = ticks / ppq;
|
|
26
|
+
return (beats * microsecondsPerBeat) / 1000;
|
|
27
|
+
}
|
|
28
|
+
export const asSpans = (file, readLinks = false) => {
|
|
29
|
+
const resultingSpans = [];
|
|
30
|
+
const tempoMap = [];
|
|
31
|
+
const currentSpans = [];
|
|
32
|
+
let bufferedMetaText;
|
|
33
|
+
for (let i = 0; i < file.tracks.length; i++) {
|
|
34
|
+
const track = file.tracks[i];
|
|
35
|
+
let currentTime = 0;
|
|
36
|
+
for (const event of track) {
|
|
37
|
+
currentTime += event.deltaTime;
|
|
38
|
+
if (event.type === 'meta' && event.subtype === 'setTempo') {
|
|
39
|
+
tempoMap.push({
|
|
40
|
+
atTick: currentTime,
|
|
41
|
+
microsecondsPerBeat: event.microsecondsPerBeat
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
if (readLinks && event.type === 'meta' && event.subtype === 'text') {
|
|
45
|
+
bufferedMetaText = event.text;
|
|
46
|
+
}
|
|
47
|
+
else if (isNoteOn(event) || isPedalOn(event) || isSoftPedalOn(event)) {
|
|
48
|
+
const type = isNoteOn(event) ? 'note' : isPedalOn(event) ? 'sustain' : 'soft';
|
|
49
|
+
const currentTempo = tempoMap.slice().reverse().find(tempo => tempo.atTick <= currentTime);
|
|
50
|
+
if (!currentTempo) {
|
|
51
|
+
console.log('No tempo event found. Skipping');
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const onsetMs = midiTickToMilliseconds(currentTime, currentTempo.microsecondsPerBeat, file.header.ticksPerBeat);
|
|
55
|
+
const link = bufferedMetaText;
|
|
56
|
+
if (type === 'note') {
|
|
57
|
+
const pitch = event.noteNumber;
|
|
58
|
+
currentSpans.push({
|
|
59
|
+
type,
|
|
60
|
+
id: `${i}-${currentTime}-${pitch}`,
|
|
61
|
+
onset: currentTime,
|
|
62
|
+
offset: 0,
|
|
63
|
+
velocity: event.velocity,
|
|
64
|
+
pitch,
|
|
65
|
+
channel: i,
|
|
66
|
+
onsetMs,
|
|
67
|
+
offsetMs: 0,
|
|
68
|
+
link
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
currentSpans.push({
|
|
73
|
+
type,
|
|
74
|
+
id: `${i}-${currentTime}-${type}`,
|
|
75
|
+
onset: currentTime,
|
|
76
|
+
offset: 0,
|
|
77
|
+
onsetMs,
|
|
78
|
+
offsetMs: 0,
|
|
79
|
+
link
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
bufferedMetaText = undefined;
|
|
83
|
+
}
|
|
84
|
+
else if (isNoteOff(event) || isPedalOff(event) || isSoftPedalOff(event)) {
|
|
85
|
+
const type = isNoteOff(event) ? 'note' : isPedalOff(event) ? 'sustain' : 'soft';
|
|
86
|
+
const currentTempo = tempoMap.slice().reverse().find(tempo => tempo.atTick <= currentTime);
|
|
87
|
+
if (!currentTempo) {
|
|
88
|
+
console.log('No tempo event found. Skipping');
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const counterpart = isNoteOff(event)
|
|
92
|
+
? currentSpans.find(e => e.type === 'note' && e.pitch === event.noteNumber)
|
|
93
|
+
: currentSpans.find(e => e.type === type);
|
|
94
|
+
if (!counterpart) {
|
|
95
|
+
console.log('Found an off event of type', type, 'at', currentTime, 'without a previous on.', 'Event:', event, 'Current spans: ', currentSpans.map(span => span.type).join(' '));
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
counterpart.offset = currentTime;
|
|
99
|
+
counterpart.offsetMs = midiTickToMilliseconds(currentTime, currentTempo.microsecondsPerBeat, file.header.ticksPerBeat);
|
|
100
|
+
if (bufferedMetaText && counterpart.link) {
|
|
101
|
+
counterpart.link += ` ${bufferedMetaText}`;
|
|
102
|
+
}
|
|
103
|
+
resultingSpans.push(counterpart);
|
|
104
|
+
currentSpans.splice(currentSpans.indexOf(counterpart), 1);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return resultingSpans
|
|
109
|
+
.filter(span => span.offsetMs > span.onsetMs)
|
|
110
|
+
.sort((a, b) => a.onset - b.onset);
|
|
111
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Original code by Craig Stuart Sapp <craig@ccrma.stanford.edu>.
|
|
3
|
+
*/
|
|
4
|
+
interface ParserOptions {
|
|
5
|
+
forceKeyCase?: 'lowercase' | 'uppercase';
|
|
6
|
+
onlyChildToRoot?: boolean;
|
|
7
|
+
}
|
|
8
|
+
interface ParserState {
|
|
9
|
+
action?: any;
|
|
10
|
+
label?: any;
|
|
11
|
+
labelbegin?: any;
|
|
12
|
+
labelend?: any;
|
|
13
|
+
curobj: any;
|
|
14
|
+
curobjname?: any;
|
|
15
|
+
curkey?: any;
|
|
16
|
+
ocurkey?: any;
|
|
17
|
+
newkey?: any;
|
|
18
|
+
onewkey?: any;
|
|
19
|
+
newvalue?: any;
|
|
20
|
+
linenum?: any;
|
|
21
|
+
node: any[];
|
|
22
|
+
typer?: any;
|
|
23
|
+
output?: any;
|
|
24
|
+
keycase?: 'lowercase' | 'uppercase';
|
|
25
|
+
}
|
|
26
|
+
export declare class AtonParser {
|
|
27
|
+
options: ParserOptions;
|
|
28
|
+
constructor(options?: ParserOptions);
|
|
29
|
+
/**
|
|
30
|
+
* parse ATON content and return the JSON that it describes.
|
|
31
|
+
*/
|
|
32
|
+
parse(str: string): any;
|
|
33
|
+
/**
|
|
34
|
+
* Parse ATON data from a list of individual records.
|
|
35
|
+
*/
|
|
36
|
+
private parseRecordArray;
|
|
37
|
+
/**
|
|
38
|
+
* Read an individual ATON record and process
|
|
39
|
+
* according to the state given as the second parameter.
|
|
40
|
+
*/
|
|
41
|
+
parseRecord(line: string, state: ParserState): void;
|
|
42
|
+
/**
|
|
43
|
+
* Remove whitespace from beginning and ending of value.
|
|
44
|
+
* If the type should be cast to another form, also do that.
|
|
45
|
+
*/
|
|
46
|
+
cleanParameter(v: ParserState): void;
|
|
47
|
+
}
|
|
48
|
+
export {};
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Original code by Craig Stuart Sapp <craig@ccrma.stanford.edu>.
|
|
3
|
+
*/
|
|
4
|
+
const defaultOptions = {
|
|
5
|
+
onlyChildToRoot: false
|
|
6
|
+
};
|
|
7
|
+
export class AtonParser {
|
|
8
|
+
constructor(options) {
|
|
9
|
+
Object.defineProperty(this, "options", {
|
|
10
|
+
enumerable: true,
|
|
11
|
+
configurable: true,
|
|
12
|
+
writable: true,
|
|
13
|
+
value: void 0
|
|
14
|
+
});
|
|
15
|
+
this.options = options || defaultOptions;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* parse ATON content and return the JSON that it describes.
|
|
19
|
+
*/
|
|
20
|
+
parse(str) {
|
|
21
|
+
const output = this.parseRecordArray(str.split(/\n/));
|
|
22
|
+
if (this.options.onlyChildToRoot) {
|
|
23
|
+
const keys = Object.keys(output);
|
|
24
|
+
if ((keys.length === 1) && (typeof output[keys[0]] === 'object')) {
|
|
25
|
+
return output[keys[0]];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return output;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Parse ATON data from a list of individual records.
|
|
32
|
+
*/
|
|
33
|
+
parseRecordArray(records) {
|
|
34
|
+
if (records.length === 0)
|
|
35
|
+
return;
|
|
36
|
+
const state = {
|
|
37
|
+
curobj: {},
|
|
38
|
+
output: {},
|
|
39
|
+
node: [],
|
|
40
|
+
keycase: this.options.forceKeyCase
|
|
41
|
+
};
|
|
42
|
+
state.curobj = state.output;
|
|
43
|
+
for (let i = 0; i < records.length; i++) {
|
|
44
|
+
state.linenum = i + 1;
|
|
45
|
+
try {
|
|
46
|
+
this.parseRecord(records[i], state);
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
console.log(error);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Remove whitespace around last property value:
|
|
54
|
+
this.cleanParameter(state);
|
|
55
|
+
return state.curobj;
|
|
56
|
+
}
|
|
57
|
+
;
|
|
58
|
+
/**
|
|
59
|
+
* Read an individual ATON record and process
|
|
60
|
+
* according to the state given as the second parameter.
|
|
61
|
+
*/
|
|
62
|
+
parseRecord(line, state) {
|
|
63
|
+
//console.log('parsing record', line)
|
|
64
|
+
let matches;
|
|
65
|
+
if (line.match(/^@{5,}|^@+\s|^@{1,4}$/)) {
|
|
66
|
+
// Filter out comment lines.
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
else if ((typeof state.curkey === 'undefined') && line.match(/^[^@]|^$/)) {
|
|
70
|
+
// Ignore unassociated text.
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
else if (matches = line.match(/^@@[^@ ]/)) {
|
|
74
|
+
// Control message.
|
|
75
|
+
// End current property.
|
|
76
|
+
this.cleanParameter(state);
|
|
77
|
+
state.curkey = undefined;
|
|
78
|
+
state.ocurkey = undefined;
|
|
79
|
+
if (matches = line.match(/^@@(BEGIN|START)\s*:\s*(.*)\s*$/i)) {
|
|
80
|
+
state.label = matches[2];
|
|
81
|
+
//console.log(state.label)
|
|
82
|
+
if (typeof state.curobj[state.label] === 'undefined') {
|
|
83
|
+
// create a new object and enter into it
|
|
84
|
+
state.curobj[state.label] = {};
|
|
85
|
+
state.node.push({ label: state.label, startline: state.linenum });
|
|
86
|
+
state.curobj = state.curobj[state.label];
|
|
87
|
+
}
|
|
88
|
+
else if (state.curobj[state.label] instanceof Array) {
|
|
89
|
+
// Append at end of array of objects with same v.label and
|
|
90
|
+
// update the array index in the last v.node entry.
|
|
91
|
+
state.curobj[state.label].push({});
|
|
92
|
+
state.node.push({});
|
|
93
|
+
state.node[state.node.length - 1].index = state.curobj[state.label].length - 1;
|
|
94
|
+
state.node[state.node.length - 1].label = state.label;
|
|
95
|
+
state.node[state.node.length - 1].startline = state.linenum;
|
|
96
|
+
state.curobj = state.curobj[state.label][state.curobj[state.label].length - 1];
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
// Single string value already exists. Convert it to an array
|
|
100
|
+
// and then append new object and enter it.
|
|
101
|
+
// var temp = state.curobj[state.label];
|
|
102
|
+
state.curobj[state.label] = [state.curobj[state.label], {}];
|
|
103
|
+
state.node.push({});
|
|
104
|
+
state.node[state.node.length - 1].index = state.curobj[state.label].length - 1;
|
|
105
|
+
state.node[state.node.length - 1].label = state.label;
|
|
106
|
+
state.node[state.node.length - 1].startline = state.linenum;
|
|
107
|
+
state.curobj = state.curobj[state.label][state.curobj[state.label].length - 1];
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
else if (matches = line.match(/^@@(END|STOP)\s*:?\s*(.*)\s*$/i)) {
|
|
111
|
+
// End an object, so go back to the parent.
|
|
112
|
+
if (typeof state.curkey !== 'undefined') {
|
|
113
|
+
// clean whitespace of last read property:
|
|
114
|
+
state.curobj[state.curkey] = state.curobj[state.curkey].replace(/^\s+|\s+$/g, '');
|
|
115
|
+
state.curkey = undefined;
|
|
116
|
+
state.ocurkey = undefined;
|
|
117
|
+
}
|
|
118
|
+
state.action = matches[1];
|
|
119
|
+
state.labelend = matches[2];
|
|
120
|
+
state.labelbegin = state.node[state.node.length - 1].label;
|
|
121
|
+
if (typeof state.node[state.node.length - 1].startline === 'undefined') {
|
|
122
|
+
throw new Error('No start for ' + state.action + ' tag on line '
|
|
123
|
+
+ state.node[state.node.length - 1].startline + ': ' + line);
|
|
124
|
+
state.output = {};
|
|
125
|
+
// return v.output;
|
|
126
|
+
}
|
|
127
|
+
if (typeof state.labelend !== 'undefined') {
|
|
128
|
+
// ensure that the v.label begin/end tags match
|
|
129
|
+
if ((state.labelbegin !== state.labelend) && (state.labelend !== "")) {
|
|
130
|
+
throw new Error('Labels do not match on lines '
|
|
131
|
+
+ state.node[state.node.length - 1].startline + ' and '
|
|
132
|
+
+ state.linenum + ': "' + state.labelbegin
|
|
133
|
+
+ '" compared to "' + state.labelend + '".');
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// Go back to the parent object.
|
|
137
|
+
if (!state.node) {
|
|
138
|
+
throw new Error('Error on line ' + state.linenum +
|
|
139
|
+
': already at object root.');
|
|
140
|
+
}
|
|
141
|
+
state.node.pop();
|
|
142
|
+
state.curobj = state.node.reduce(function (obj, x) {
|
|
143
|
+
return (obj[x.label] instanceof Array) ?
|
|
144
|
+
obj[x.label][x.index] : obj[x.label];
|
|
145
|
+
}, state.output);
|
|
146
|
+
}
|
|
147
|
+
else if (matches = line.match(/^@@TYPE\s*:\s*([^:]+)\s*:\s*(.*)\s*$/i)) {
|
|
148
|
+
// Automatic property value conversion.
|
|
149
|
+
state.typer[matches[1]] = matches[2];
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
else if (matches = line.match(/^@([^\s:@][^:]*)\s*:\s*(.*)\s*$/)) {
|
|
153
|
+
// New property
|
|
154
|
+
state.newkey = matches[1];
|
|
155
|
+
state.onewkey = state.newkey;
|
|
156
|
+
state.newvalue = matches[2];
|
|
157
|
+
this.cleanParameter(state);
|
|
158
|
+
if (state.keycase === 'uppercase') {
|
|
159
|
+
state.ocurkey = state.newkey;
|
|
160
|
+
state.curkey = state.newkey.toUpperCase();
|
|
161
|
+
}
|
|
162
|
+
else if (state.keycase === 'lowercase') {
|
|
163
|
+
state.ocurkey = state.newkey;
|
|
164
|
+
state.curkey = state.newkey.toLowerCase();
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
state.ocurkey = state.newkey;
|
|
168
|
+
state.curkey = state.newkey;
|
|
169
|
+
}
|
|
170
|
+
if (typeof state.curobj[state.curkey] === 'undefined') {
|
|
171
|
+
// create a new property
|
|
172
|
+
state.curobj[state.curkey] = state.newvalue;
|
|
173
|
+
}
|
|
174
|
+
else if (state.curobj[state.curkey] instanceof Array) {
|
|
175
|
+
// append next object to end of array
|
|
176
|
+
state.curobj[state.curkey].push(state.newvalue);
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
// convert property value to array, and then append
|
|
180
|
+
state.curobj[state.curkey] = [state.curobj[state.curkey], state.newvalue];
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
else if (typeof state.curkey !== 'undefined') {
|
|
184
|
+
// Continuing value from property started previously
|
|
185
|
+
// If the line starts with a backslash, remove it since it is an
|
|
186
|
+
//escape for the "@" sign or a literal "\" at the start of a line:
|
|
187
|
+
// \@some data line -=> @some data line
|
|
188
|
+
// \\@some data line -=> \@some data line
|
|
189
|
+
// Only "@" and "\@" at the start of the line need to be esacaped;
|
|
190
|
+
// otherwise, all other "@" and "\" characters are literal.
|
|
191
|
+
// If another property marker is used other than "@", then that
|
|
192
|
+
// character (or string) needs to be backslash escaped at the
|
|
193
|
+
// start of a multi-line value line.
|
|
194
|
+
if (line.charAt(0) !== '@') {
|
|
195
|
+
if (line.slice(0, 2) === '\\@') {
|
|
196
|
+
line = line.slice(1);
|
|
197
|
+
}
|
|
198
|
+
if (line.slice(0, 3) === '\\\\@') {
|
|
199
|
+
line = line.slice(1);
|
|
200
|
+
}
|
|
201
|
+
if (state.curobj && state.curobj[state.curkey] instanceof Array) {
|
|
202
|
+
state.curobj[state.curkey][state.curobj[state.curkey].length - 1] += '\n' + line;
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
state.curobj[state.curkey] += '\n' + line;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
;
|
|
211
|
+
/**
|
|
212
|
+
* Remove whitespace from beginning and ending of value.
|
|
213
|
+
* If the type should be cast to another form, also do that.
|
|
214
|
+
*/
|
|
215
|
+
cleanParameter(v) {
|
|
216
|
+
if ((typeof v.curkey !== 'undefined') && v.curobj && v.curobj[v.curkey]) {
|
|
217
|
+
var value;
|
|
218
|
+
if (v.curobj[v.curkey] instanceof Array) {
|
|
219
|
+
value = v.curobj[v.curkey][v.curobj[v.curkey].length - 1];
|
|
220
|
+
}
|
|
221
|
+
else if (typeof v.curobj[v.curkey] === 'string') {
|
|
222
|
+
value = v.curobj[v.curkey];
|
|
223
|
+
}
|
|
224
|
+
value = value.replace(/^\s+|\s+$/g, '');
|
|
225
|
+
if (v.typer && (typeof v.typer[v.ocurkey] !== 'undefined')) {
|
|
226
|
+
var newtype = v.typer[v.ocurkey];
|
|
227
|
+
if (newtype.match(/number/i)) {
|
|
228
|
+
value = Number(value);
|
|
229
|
+
}
|
|
230
|
+
else if (newtype.match(/integer/i)) {
|
|
231
|
+
value = parseInt(value);
|
|
232
|
+
}
|
|
233
|
+
else if (newtype.match(/json/i)) {
|
|
234
|
+
value = JSON.parse(value);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (v.curobj[v.curkey] instanceof Array) {
|
|
238
|
+
v.curobj[v.curkey][v.curobj[v.curkey].length - 1] = value;
|
|
239
|
+
}
|
|
240
|
+
else if (typeof v.curobj[v.curkey] === 'string') {
|
|
241
|
+
v.curobj[v.curkey] = value;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { expect, test } from 'vitest';
|
|
2
|
+
import { AtonParser } from './AtonParser';
|
|
3
|
+
test('it import ATON files correctly', () => {
|
|
4
|
+
const parser = new AtonParser();
|
|
5
|
+
const result = parser.parse(`
|
|
6
|
+
@key1: value1
|
|
7
|
+
@@START: key2
|
|
8
|
+
@key2a: value2a
|
|
9
|
+
@key2b: value2b
|
|
10
|
+
@key2c: value2c
|
|
11
|
+
@@END: key2
|
|
12
|
+
@key3: value3`);
|
|
13
|
+
expect(result.key1).toBe('value1');
|
|
14
|
+
expect(result.key2.key2a).toBe('value2a');
|
|
15
|
+
expect(result.key3).toBe('value3');
|
|
16
|
+
});
|