@dsebastien/music-notation 0.0.0-stage → 0.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 +21 -0
- package/README.md +107 -2
- package/dist/THIRD_PARTY_LICENSES.md +57 -0
- package/dist/blocks/block-source.d.ts +12 -0
- package/dist/blocks/note-set-field.d.ts +8 -0
- package/dist/blocks/renderers.d.ts +16 -0
- package/dist/chord/guitar.d.ts +24 -0
- package/dist/chords/chart.d.ts +6 -0
- package/dist/chords/chordpro.d.ts +39 -0
- package/dist/chunks/index-b71brbpf.js +1028 -0
- package/dist/chunks/index-b71brbpf.js.map +23 -0
- package/dist/chunks/index-y8a78sfx.js +50 -0
- package/dist/chunks/index-y8a78sfx.js.map +10 -0
- package/dist/fretboard/fretboard.d.ts +10 -0
- package/dist/fretboard/tuning.d.ts +5 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +281 -0
- package/dist/index.js.map +10 -0
- package/dist/keyboard/keyboard.d.ts +14 -0
- package/dist/music-notation.css +190 -0
- package/dist/music-notation.publish.min.js +192 -0
- package/dist/publish/auto.d.ts +15 -0
- package/dist/publish/index.d.ts +5 -0
- package/dist/publish/index.js +81 -0
- package/dist/publish/index.js.map +10 -0
- package/dist/render/dom.d.ts +12 -0
- package/dist/render/render-block.d.ts +14 -0
- package/dist/styles.d.ts +4 -0
- package/dist/theory/chord-symbol.d.ts +10 -0
- package/dist/theory/note-set.d.ts +32 -0
- package/dist/theory/pitch.d.ts +27 -0
- package/dist/theory/transpose.d.ts +9 -0
- package/dist/voicings/index.d.ts +6 -0
- package/dist/voicings/index.js +78 -0
- package/dist/voicings/index.js.map +10 -0
- package/package.json +131 -5
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The browser bundle (`music-notation.publish.min.js`): injects the styles and renders every
|
|
3
|
+
* notation block on the page, now and as it changes. Exposes the API as `window.MusicNotation`.
|
|
4
|
+
*/
|
|
5
|
+
import * as api from '../index.js';
|
|
6
|
+
import { lookupGuitarVoicing } from '../voicings/index.js';
|
|
7
|
+
declare global {
|
|
8
|
+
interface Window {
|
|
9
|
+
musicNotationLoaded?: boolean;
|
|
10
|
+
MusicNotation?: typeof api & {
|
|
11
|
+
lookupGuitarVoicing: typeof lookupGuitarVoicing;
|
|
12
|
+
stop: () => void;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { RenderOptions } from '../blocks/renderers.js';
|
|
2
|
+
/** Render every block not rendered yet (or changed, or whose rendering vanished) under `root`. */
|
|
3
|
+
export declare function renderAll(root: ParentNode, options?: RenderOptions): number;
|
|
4
|
+
/** Render now and whenever the page changes. Returns a function that stops watching. */
|
|
5
|
+
export declare function autoRender(doc: Document, options?: RenderOptions): () => void;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import {
|
|
2
|
+
LANGUAGES2,
|
|
3
|
+
renderBlock2
|
|
4
|
+
} from "../chunks/index-b71brbpf.js";
|
|
5
|
+
|
|
6
|
+
// src/publish/index.ts
|
|
7
|
+
var SELECTOR = LANGUAGES2.map((l) => `pre > code.language-${l}`).join(", ");
|
|
8
|
+
var MARK = "mpRendered";
|
|
9
|
+
var SOURCE = "mpSource";
|
|
10
|
+
var languageOf = (code) => LANGUAGES2.find((l) => code.classList.contains(`language-${l}`));
|
|
11
|
+
function fingerprint(text) {
|
|
12
|
+
let hash = 5381;
|
|
13
|
+
for (let i = 0;i < text.length; i++)
|
|
14
|
+
hash = (hash * 33 ^ text.charCodeAt(i)) >>> 0;
|
|
15
|
+
return `${text.length}:${hash.toString(36)}`;
|
|
16
|
+
}
|
|
17
|
+
var renderingOf = (pre) => {
|
|
18
|
+
const next = pre.nextElementSibling;
|
|
19
|
+
return next?.classList.contains("mn-block") ? next : null;
|
|
20
|
+
};
|
|
21
|
+
function renderAll(root, options = {}) {
|
|
22
|
+
let count = 0;
|
|
23
|
+
root.querySelectorAll(SELECTOR).forEach((code) => {
|
|
24
|
+
const pre = code.parentElement;
|
|
25
|
+
const language = languageOf(code);
|
|
26
|
+
if (!pre?.parentElement || !language)
|
|
27
|
+
return;
|
|
28
|
+
const source = code.textContent ?? "";
|
|
29
|
+
const print = fingerprint(source);
|
|
30
|
+
const previous = renderingOf(pre);
|
|
31
|
+
if (pre.dataset[MARK] && pre.dataset[SOURCE] === print && previous)
|
|
32
|
+
return;
|
|
33
|
+
previous?.remove();
|
|
34
|
+
pre.style.removeProperty("display");
|
|
35
|
+
pre.dataset[SOURCE] = print;
|
|
36
|
+
pre.dataset[MARK] = "pending";
|
|
37
|
+
const holder = pre.ownerDocument.createElement("div");
|
|
38
|
+
const result = renderBlock2(language, source, holder, options);
|
|
39
|
+
pre.insertAdjacentElement("afterend", result.el);
|
|
40
|
+
if (!result.ok) {
|
|
41
|
+
pre.dataset[MARK] = "error";
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
pre.style.display = "none";
|
|
45
|
+
pre.dataset[MARK] = "done";
|
|
46
|
+
count++;
|
|
47
|
+
});
|
|
48
|
+
return count;
|
|
49
|
+
}
|
|
50
|
+
function autoRender(doc, options = {}) {
|
|
51
|
+
renderAll(doc, options);
|
|
52
|
+
const view = doc.defaultView;
|
|
53
|
+
if (!view)
|
|
54
|
+
return () => {
|
|
55
|
+
return;
|
|
56
|
+
};
|
|
57
|
+
let scheduled = false;
|
|
58
|
+
let stopped = false;
|
|
59
|
+
const observer = new view.MutationObserver(() => {
|
|
60
|
+
if (scheduled || stopped)
|
|
61
|
+
return;
|
|
62
|
+
scheduled = true;
|
|
63
|
+
view.requestAnimationFrame(() => {
|
|
64
|
+
scheduled = false;
|
|
65
|
+
if (!stopped)
|
|
66
|
+
renderAll(doc, options);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
observer.observe(doc.body, { childList: true, subtree: true, characterData: true });
|
|
70
|
+
return () => {
|
|
71
|
+
stopped = true;
|
|
72
|
+
observer.disconnect();
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
export {
|
|
76
|
+
autoRender,
|
|
77
|
+
renderAll
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
//# debugId=25709619E83183AF64756E2164756E21
|
|
81
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/publish/index.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * Obsidian Publish (and any page): turn `<pre><code class=\"language-chord\">` blocks into\n * notation. Idempotent (`data-mp-rendered`), follows the page as it changes (Publish swaps and\n * virtualises content), and hides the source only after a successful render.\n */\nimport { LANGUAGES, renderBlock } from '../render/render-block.js'\nimport type { NotationLanguage, RenderOptions } from '../blocks/renderers.js'\n\nconst SELECTOR = LANGUAGES.map((l) => `pre > code.language-${l}`).join(', ')\nconst MARK = 'mpRendered'\nconst SOURCE = 'mpSource'\n\nconst languageOf = (code: Element): NotationLanguage | undefined =>\n LANGUAGES.find((l) => code.classList.contains(`language-${l}`))\n\n/** A short fingerprint of a block's source, to notice edits in place. */\nfunction fingerprint(text: string): string {\n let hash = 5381\n for (let i = 0; i < text.length; i++) hash = ((hash * 33) ^ text.charCodeAt(i)) >>> 0\n return `${text.length}:${hash.toString(36)}`\n}\n\n/** The rendering that follows a `<pre>`, if it's still there. */\nconst renderingOf = (pre: HTMLElement): Element | null => {\n const next = pre.nextElementSibling\n return next?.classList.contains('mn-block') ? next : null\n}\n\n/** Render every block not rendered yet (or changed, or whose rendering vanished) under `root`. */\nexport function renderAll(root: ParentNode, options: RenderOptions = {}): number {\n let count = 0\n root.querySelectorAll(SELECTOR).forEach((code) => {\n const pre = code.parentElement\n const language = languageOf(code)\n if (!pre?.parentElement || !language) return\n const source = code.textContent ?? ''\n const print = fingerprint(source)\n const previous = renderingOf(pre)\n if (pre.dataset[MARK] && pre.dataset[SOURCE] === print && previous) return\n // New, edited, or its rendering was removed: (re)render from scratch.\n previous?.remove()\n pre.style.removeProperty('display')\n pre.dataset[SOURCE] = print\n pre.dataset[MARK] = 'pending'\n const holder = pre.ownerDocument.createElement('div')\n const result = renderBlock(language, source, holder, options)\n pre.insertAdjacentElement('afterend', result.el)\n if (!result.ok) {\n // Leave the source visible; the error explains what's wrong.\n pre.dataset[MARK] = 'error'\n return\n }\n pre.style.display = 'none'\n pre.dataset[MARK] = 'done'\n count++\n })\n return count\n}\n\n/** Render now and whenever the page changes. Returns a function that stops watching. */\nexport function autoRender(doc: Document, options: RenderOptions = {}): () => void {\n renderAll(doc, options)\n const view = doc.defaultView\n if (!view) return () => undefined\n let scheduled = false\n let stopped = false\n const observer = new view.MutationObserver(() => {\n if (scheduled || stopped) return\n scheduled = true\n // One pass per frame, however many mutations Publish makes.\n view.requestAnimationFrame(() => {\n scheduled = false\n if (!stopped) renderAll(doc, options)\n })\n })\n observer.observe(doc.body, { childList: true, subtree: true, characterData: true })\n return () => {\n stopped = true\n observer.disconnect()\n }\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": ";;;;;;AAQA,IAAM,WAAW,WAAU,IAAI,CAAC,MAAM,uBAAuB,GAAG,EAAE,KAAK,IAAI;AAC3E,IAAM,OAAO;AACb,IAAM,SAAS;AAEf,IAAM,aAAa,CAAC,SAChB,WAAU,KAAK,CAAC,MAAM,KAAK,UAAU,SAAS,YAAY,GAAG,CAAC;AAGlE,SAAS,WAAW,CAAC,MAAsB;AAAA,EACvC,IAAI,OAAO;AAAA,EACX,SAAS,IAAI,EAAG,IAAI,KAAK,QAAQ;AAAA,IAAK,QAAS,OAAO,KAAM,KAAK,WAAW,CAAC,OAAO;AAAA,EACpF,OAAO,GAAG,KAAK,UAAU,KAAK,SAAS,EAAE;AAAA;AAI7C,IAAM,cAAc,CAAC,QAAqC;AAAA,EACtD,MAAM,OAAO,IAAI;AAAA,EACjB,OAAO,MAAM,UAAU,SAAS,UAAU,IAAI,OAAO;AAAA;AAIlD,SAAS,SAAS,CAAC,MAAkB,UAAyB,CAAC,GAAW;AAAA,EAC7E,IAAI,QAAQ;AAAA,EACZ,KAAK,iBAAiB,QAAQ,EAAE,QAAQ,CAAC,SAAS;AAAA,IAC9C,MAAM,MAAM,KAAK;AAAA,IACjB,MAAM,WAAW,WAAW,IAAI;AAAA,IAChC,IAAI,CAAC,KAAK,iBAAiB,CAAC;AAAA,MAAU;AAAA,IACtC,MAAM,SAAS,KAAK,eAAe;AAAA,IACnC,MAAM,QAAQ,YAAY,MAAM;AAAA,IAChC,MAAM,WAAW,YAAY,GAAG;AAAA,IAChC,IAAI,IAAI,QAAQ,SAAS,IAAI,QAAQ,YAAY,SAAS;AAAA,MAAU;AAAA,IAEpE,UAAU,OAAO;AAAA,IACjB,IAAI,MAAM,eAAe,SAAS;AAAA,IAClC,IAAI,QAAQ,UAAU;AAAA,IACtB,IAAI,QAAQ,QAAQ;AAAA,IACpB,MAAM,SAAS,IAAI,cAAc,cAAc,KAAK;AAAA,IACpD,MAAM,SAAS,aAAY,UAAU,QAAQ,QAAQ,OAAO;AAAA,IAC5D,IAAI,sBAAsB,YAAY,OAAO,EAAE;AAAA,IAC/C,IAAI,CAAC,OAAO,IAAI;AAAA,MAEZ,IAAI,QAAQ,QAAQ;AAAA,MACpB;AAAA,IACJ;AAAA,IACA,IAAI,MAAM,UAAU;AAAA,IACpB,IAAI,QAAQ,QAAQ;AAAA,IACpB;AAAA,GACH;AAAA,EACD,OAAO;AAAA;AAIJ,SAAS,UAAU,CAAC,KAAe,UAAyB,CAAC,GAAe;AAAA,EAC/E,UAAU,KAAK,OAAO;AAAA,EACtB,MAAM,OAAO,IAAI;AAAA,EACjB,IAAI,CAAC;AAAA,IAAM,OAAO,MAAG;AAAA,MAAG;AAAA;AAAA,EACxB,IAAI,YAAY;AAAA,EAChB,IAAI,UAAU;AAAA,EACd,MAAM,WAAW,IAAI,KAAK,iBAAiB,MAAM;AAAA,IAC7C,IAAI,aAAa;AAAA,MAAS;AAAA,IAC1B,YAAY;AAAA,IAEZ,KAAK,sBAAsB,MAAM;AAAA,MAC7B,YAAY;AAAA,MACZ,IAAI,CAAC;AAAA,QAAS,UAAU,KAAK,OAAO;AAAA,KACvC;AAAA,GACJ;AAAA,EACD,SAAS,QAAQ,IAAI,MAAM,EAAE,WAAW,MAAM,SAAS,MAAM,eAAe,KAAK,CAAC;AAAA,EAClF,OAAO,MAAM;AAAA,IACT,UAAU;AAAA,IACV,SAAS,WAAW;AAAA;AAAA;",
|
|
8
|
+
"debugId": "25709619E83183AF64756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** DOM and SVG helpers. Everything is created in the host's document (popout windows, Publish). */
|
|
2
|
+
export declare const SVG_NS = "http://www.w3.org/2000/svg";
|
|
3
|
+
type Attributes = Record<string, string | number | undefined>;
|
|
4
|
+
/** An SVG element with attributes and children. */
|
|
5
|
+
export declare function svg(doc: Document, tag: string, attrs?: Attributes, children?: (Element | null)[]): SVGElement;
|
|
6
|
+
/** An SVG <text>. */
|
|
7
|
+
export declare function svgText(doc: Document, text: string, attrs?: Attributes): SVGElement;
|
|
8
|
+
/** An HTML element with a class and optional text. */
|
|
9
|
+
export declare function html<K extends keyof HTMLElementTagNameMap>(doc: Document, tag: K, cls?: string, text?: string): HTMLElementTagNameMap[K];
|
|
10
|
+
/** A root <svg> sized by its viewBox, scaling with the container up to its natural width. */
|
|
11
|
+
export declare function svgRoot(doc: Document, width: number, height: number, cls: string, label: string): SVGElement;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Render a notation block into a host element. Never throws: errors render inline. */
|
|
2
|
+
import { type NotationLanguage, type RenderOptions } from '../blocks/renderers.js';
|
|
3
|
+
export declare const LANGUAGES: NotationLanguage[];
|
|
4
|
+
export declare const isNotationLanguage: (language: string) => language is NotationLanguage;
|
|
5
|
+
export interface RenderResult {
|
|
6
|
+
ok: boolean;
|
|
7
|
+
/** The block's container, appended to the host. */
|
|
8
|
+
el: HTMLElement;
|
|
9
|
+
/** The message shown when ok is false. */
|
|
10
|
+
error: string | null;
|
|
11
|
+
/** Removes what was rendered. */
|
|
12
|
+
destroy(): void;
|
|
13
|
+
}
|
|
14
|
+
export declare function renderBlock(language: NotationLanguage, source: string, host: HTMLElement, options?: RenderOptions): RenderResult;
|
package/dist/styles.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface ParsedChord {
|
|
2
|
+
symbol: string;
|
|
3
|
+
root: string;
|
|
4
|
+
quality: string;
|
|
5
|
+
bass: string | null;
|
|
6
|
+
/** "F Sharp Minor 7", "C Major over G". */
|
|
7
|
+
name: string;
|
|
8
|
+
}
|
|
9
|
+
/** Parse a chord symbol; null when it isn't one. */
|
|
10
|
+
export declare function parseChord(symbol: string): ParsedChord | null;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type NoteLabel = 'notes' | 'degrees' | 'intervals' | 'fingers' | 'none';
|
|
2
|
+
export interface NoteSetMember {
|
|
3
|
+
chroma: number;
|
|
4
|
+
/** As spelled by the chord, scale or list ("Bb", not "A#"). */
|
|
5
|
+
name: string;
|
|
6
|
+
/** 1-based position in the chord or scale; null for a plain list. */
|
|
7
|
+
degree: number | null;
|
|
8
|
+
/** "1P", "3m", "5P"…; null for a plain list. */
|
|
9
|
+
interval: string | null;
|
|
10
|
+
}
|
|
11
|
+
export interface NoteSet {
|
|
12
|
+
/** "A minor pentatonic", "Cmaj7", or the notes as written. */
|
|
13
|
+
title: string;
|
|
14
|
+
members: NoteSetMember[];
|
|
15
|
+
/** Chroma of the root; null for a plain list. */
|
|
16
|
+
root: number | null;
|
|
17
|
+
/** Exact pitches (MIDI), when the notes were given with octaves ("C4 E4 G4"). */
|
|
18
|
+
pitches: number[] | null;
|
|
19
|
+
preferFlats: boolean;
|
|
20
|
+
}
|
|
21
|
+
/** The member a pitch belongs to, if any. */
|
|
22
|
+
export declare const memberOf: (set: NoteSet, midi: number) => NoteSetMember | undefined;
|
|
23
|
+
/** tonal's "3m", "5d", "4A", "1P" → how musicians write them: "b3", "b5", "#4", "R". */
|
|
24
|
+
export declare function intervalLabel(interval: string): string;
|
|
25
|
+
/** Text shown on a highlighted note. */
|
|
26
|
+
export declare function labelOf(member: NoteSetMember, label: NoteLabel, finger?: string): string;
|
|
27
|
+
/** Notes of a chord symbol ("Cmaj7", "C/G"); null when tonal doesn't know it. */
|
|
28
|
+
export declare function chordNoteSet(symbol: string): NoteSet | null;
|
|
29
|
+
/** Notes of a scale ("A minor pentatonic", "D dorian"); null when tonal doesn't know it. */
|
|
30
|
+
export declare function scaleNoteSet(name: string): NoteSet | null;
|
|
31
|
+
/** An explicit list: "A C E" (pitch classes) or "A3 C4 E4" (exact pitches). */
|
|
32
|
+
export declare function listNoteSet(text: string): NoteSet | null;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pitch classes and note names. A pitch class is a chroma: 0 = C … 11 = B. Names accept ASCII
|
|
3
|
+
* (`#`, `b`) and Unicode (`♯`, `♭`) accidentals.
|
|
4
|
+
*/
|
|
5
|
+
export declare const SHARP_NAMES: readonly ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
|
|
6
|
+
export declare const FLAT_NAMES: readonly ["C", "Db", "D", "Eb", "E", "F", "Gb", "G", "Ab", "A", "Bb", "B"];
|
|
7
|
+
/** "C#", "Db", "D♭", "E" → 0..11; null when it isn't a note name. */
|
|
8
|
+
export declare function chromaOf(name: string): number | null;
|
|
9
|
+
/** A note with its octave, scientific pitch notation ("C4" = MIDI 60); null when invalid. */
|
|
10
|
+
export declare function midiOf(note: string): number | null;
|
|
11
|
+
/** Name of a pitch class, with sharps or flats. Non-integers are rounded. */
|
|
12
|
+
export declare const nameOf: (chroma: number, preferFlats?: boolean) => string;
|
|
13
|
+
export interface ChordSymbolParts {
|
|
14
|
+
root: string;
|
|
15
|
+
/** Everything between the root and the bass: "m7", "6/9", "". */
|
|
16
|
+
suffix: string;
|
|
17
|
+
bass: string | null;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* "F♯m7/C#" → root F#, suffix m7, bass C#. A slash starts a bass only when a note name follows
|
|
21
|
+
* ("C6/9" has no bass). ♯ and ♭ become # and b.
|
|
22
|
+
*/
|
|
23
|
+
export declare function splitChordSymbol(symbol: string): ChordSymbolParts | null;
|
|
24
|
+
/** "C4" for MIDI 60. */
|
|
25
|
+
export declare const noteOf: (midi: number, preferFlats?: boolean) => string;
|
|
26
|
+
/** Whether a name is spelled with a flat ("Bb", "E♭"). */
|
|
27
|
+
export declare const isFlat: (name: string) => boolean;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Whether a key ("F", "Bb", "Dm") is written with flats. */
|
|
2
|
+
export declare function keyPrefersFlats(key: string): boolean;
|
|
3
|
+
/**
|
|
4
|
+
* Transpose a chord symbol ("F#m7/C#", +2 → "G#m7/D#"). Spelling follows `preferFlats`, or the
|
|
5
|
+
* symbol's own when not given. Text that isn't a chord symbol comes back unchanged.
|
|
6
|
+
*/
|
|
7
|
+
export declare function transposeChord(symbol: string, semitones: number, preferFlats?: boolean): string;
|
|
8
|
+
/** Transpose a key ("Am", +3 → "Cm"), spelled the way that key is usually written. */
|
|
9
|
+
export declare function transposeKey(key: string, semitones: number): string;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { FrettedVoicing } from '../chord/guitar.js';
|
|
2
|
+
/** Finds the n-th voicing (0-based) of a chord symbol; null when unknown. */
|
|
3
|
+
export type VoicingLookup = (symbol: string, index: number) => FrettedVoicing | null;
|
|
4
|
+
export declare const lookupGuitarVoicing: VoicingLookup;
|
|
5
|
+
/** How many voicings the database has for a symbol. */
|
|
6
|
+
export declare function voicingCount(symbol: string): number;
|