ipa-core 1.0.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/README.md +0 -0
- package/core.js +131 -0
- package/generate-config.js +170 -0
- package/generate-conlang.js +120 -0
- package/index.js +9 -0
- package/modifiers.js +250 -0
- package/package.json +15 -0
- package/parser.js +137 -0
- package/rules.js +52 -0
- package/test.js +60 -0
package/README.md
ADDED
|
Binary file
|
package/core.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// IPA elements in json notation
|
|
2
|
+
const core = {
|
|
3
|
+
|
|
4
|
+
//consonants
|
|
5
|
+
//pulmonic
|
|
6
|
+
|
|
7
|
+
//plosive
|
|
8
|
+
'p': { type: "consonant", features: { manner: "plosive", place: "bilabial", voicing: "voiceless" }, airstream: "pulmonic" },
|
|
9
|
+
'b': { type: "consonant", features: { manner: "plosive", place: "bilabial", voicing: "voiced" }, airstream: "pulmonic" },
|
|
10
|
+
't': { type: "consonant", features: { manner: "plosive", place: "alveolar", voicing: "voiceless" }, airstream: "pulmonic" },
|
|
11
|
+
'd': { type: "consonant", features: { manner: "plosive", place: "alveolar", voicing: "voiced" }, airstream: "pulmonic" },
|
|
12
|
+
'ʈ': { type: "consonant", features: { manner: "plosive", place: "retroflex", voicing: "voiceless" }, airstream: "pulmonic" },
|
|
13
|
+
'ɖ': { type: "consonant", features: { manner: "plosive", place: "retroflex", voicing: "voiced" }, airstream: "pulmonic" },
|
|
14
|
+
'c': { type: "consonant", features: { manner: "plosive", place: "palatal", voicing: "voiceless" }, airstream: "pulmonic" },
|
|
15
|
+
'ɟ': { type: "consonant", features: { manner: "plosive", place: "palatal", voicing: "voiced" }, airstream: "pulmonic" },
|
|
16
|
+
'k': { type: "consonant", features: { manner: "plosive", place: "velar", voicing: "voiceless" }, airstream: "pulmonic" },
|
|
17
|
+
'ɡ': { type: "consonant", features: { manner: "plosive", place: "velar", voicing: "voiced" }, airstream: "pulmonic" },
|
|
18
|
+
'q': { type: "consonant", features: { manner: "plosive", place: "uvular", voicing: "voiceless" }, airstream: "pulmonic" },
|
|
19
|
+
'ɢ': { type: "consonant", features: { manner: "plosive", place: "uvular", voicing: "voiced" }, airstream: "pulmonic" },
|
|
20
|
+
'ʔ': { type: "consonant", features: { manner: "plosive", place: "glottal", voicing: "voiceless" }, airstream: "pulmonic" },
|
|
21
|
+
//nasal
|
|
22
|
+
'm': { type: "consonant", features: { manner: "nasal", place: "bilabial", voicing: "voiced" }, airstream: "pulmonic" },
|
|
23
|
+
'ɱ': { type: "consonant", features: { manner: "nasal", place: "labiodental", voicing: "voiced" }, airstream: "pulmonic" },
|
|
24
|
+
'n': { type: "consonant", features: { manner: "nasal", place: "alveolar", voicing: "voiced" }, airstream: "pulmonic" },
|
|
25
|
+
'ɳ': { type: "consonant", features: { manner: "nasal", place: "retroflex", voicing: "voiced" }, airstream: "pulmonic" },
|
|
26
|
+
'ɲ': { type: "consonant", features: { manner: "nasal", place: "palatal", voicing: "voiced" }, airstream: "pulmonic" },
|
|
27
|
+
'ŋ': { type: "consonant", features: { manner: "nasal", place: "velar", voicing: "voiced" }, airstream: "pulmonic" },
|
|
28
|
+
'ɴ': { type: "consonant", features: { manner: "nasal", place: "uvular", voicing: "voiced" }, airstream: "pulmonic" },
|
|
29
|
+
//trill
|
|
30
|
+
'ʙ': { type: "consonant", features: { manner: "trill", place: "bilabial", voicing: "voiced" }, airstream: "pulmonic" },
|
|
31
|
+
'r': { type: "consonant", features: { manner: "trill", place: "alveolar", voicing: "voiced" }, airstream: "pulmonic" },
|
|
32
|
+
'ʀ': { type: "consonant", features: { manner: "trill", place: "uvular", voicing: "voiced" }, airstream: "pulmonic" },
|
|
33
|
+
//tap or flap
|
|
34
|
+
'ⱱ': { type: "consonant", features: { manner: "tap/flap", place: "labiodental", voicing: "voiced" }, airstream: "pulmonic" },
|
|
35
|
+
'ɾ': { type: "consonant", features: { manner: "tap/flap", place: "alveolar", voicing: "voiced" }, airstream: "pulmonic" },
|
|
36
|
+
'ɽ': { type: "consonant", features: { manner: "tap/flap", place: "retroflex", voicing: "voiced" }, airstream: "pulmonic" },
|
|
37
|
+
//fricative
|
|
38
|
+
'ɸ': { type: "consonant", features: { manner: "fricative", place: "bilabial", voicing: "voiceless" }, airstream: "pulmonic" },
|
|
39
|
+
'f': { type: "consonant", features: { manner: "fricative", place: "labiodental", voicing: "voiceless" }, airstream: "pulmonic" },
|
|
40
|
+
'θ': { type: "consonant", features: { manner: "fricative", place: "dental", voicing: "voiceless" }, airstream: "pulmonic" },
|
|
41
|
+
's': { type: "consonant", features: { manner: "fricative", place: "alveolar", voicing: "voiceless" }, airstream: "pulmonic" },
|
|
42
|
+
'ʃ': { type: "consonant", features: { manner: "fricative", place: "postalveolar", voicing: "voiceless" }, airstream: "pulmonic" },
|
|
43
|
+
'ʂ': { type: "consonant", features: { manner: "fricative", place: "retroflex", voicing: "voiceless" }, airstream: "pulmonic" },
|
|
44
|
+
'ç': { type: "consonant", features: { manner: "fricative", place: "palatal", voicing: "voiceless" }, airstream: "pulmonic" },
|
|
45
|
+
'x': { type: "consonant", features: { manner: "fricative", place: "velar", voicing: "voiceless" }, airstream: "pulmonic" },
|
|
46
|
+
'χ': { type: "consonant", features: { manner: "fricative", place: "uvular", voicing: "voiceless" }, airstream: "pulmonic" },
|
|
47
|
+
'ħ': { type: "consonant", features: { manner: "fricative", place: "pharyngeal", voicing: "voiceless" }, airstream: "pulmonic" },
|
|
48
|
+
'h': { type: "consonant", features: { manner: "fricative", place: "glottal", voicing: "voiceless" }, airstream: "pulmonic" },
|
|
49
|
+
'β': { type: "consonant", features: { manner: "fricative", place: "bilabial", voicing: "voiced" }, airstream: "pulmonic" },
|
|
50
|
+
'v': { type: "consonant", features: { manner: "fricative", place: "labiodental", voicing: "voiced" }, airstream: "pulmonic" },
|
|
51
|
+
'ð': { type: "consonant", features: { manner: "fricative", place: "dental", voicing: "voiced" }, airstream: "pulmonic" },
|
|
52
|
+
'z': { type: "consonant", features: { manner: "fricative", place: "alveolar", voicing: "voiced" }, airstream: "pulmonic" },
|
|
53
|
+
'ʒ': { type: "consonant", features: { manner: "fricative", place: "postalveolar", voicing: "voiced" }, airstream: "pulmonic" },
|
|
54
|
+
'ʐ': { type: "consonant", features: { manner: "fricative", place: "retroflex", voicing: "voiced" }, airstream: "pulmonic" },
|
|
55
|
+
'ʝ': { type: "consonant", features: { manner: "fricative", place: "palatal", voicing: "voiced" }, airstream: "pulmonic" },
|
|
56
|
+
'ɣ': { type: "consonant", features: { manner: "fricative", place: "velar", voicing: "voiced" }, airstream: "pulmonic" },
|
|
57
|
+
'ʁ': { type: "consonant", features: { manner: "fricative", place: "uvular", voicing: "voiced" }, airstream: "pulmonic" },
|
|
58
|
+
'ʕ': { type: "consonant", features: { manner: "fricative", place: "pharyngeal", voicing: "voiced" }, airstream: "pulmonic" },
|
|
59
|
+
'ɦ': { type: "consonant", features: { manner: "fricative", place: "glottal", voicing: "voiced" }, airstream: "pulmonic" },
|
|
60
|
+
// LATERAL FRICATIVES
|
|
61
|
+
'ɬ': { type: "consonant", features: { manner: "lateral fricative", place: "alveolar", voicing: "voiceless" }, airstream: "pulmonic" },
|
|
62
|
+
'ɮ': { type: "consonant", features: { manner: "lateral fricative", place: "alveolar", voicing: "voiced" }, airstream: "pulmonic" },
|
|
63
|
+
//aproximant
|
|
64
|
+
'ʋ': { type: "consonant", features: { manner: "approximant", place: "labiodental", voicing: "voiced" }, airstream: "pulmonic" },
|
|
65
|
+
'ɹ': { type: "consonant", features: { manner: "approximant", place: "alveolar", voicing: "voiced" }, airstream: "pulmonic" },
|
|
66
|
+
'ɻ': { type: "consonant", features: { manner: "approximant", place: "retroflex", voicing: "voiced" }, airstream: "pulmonic" },
|
|
67
|
+
'j': { type: "consonant", features: { manner: "approximant", place: "palatal", voicing: "voiced" }, airstream: "pulmonic" },
|
|
68
|
+
'ɰ': { type: "consonant", features: { manner: "approximant", place: "velar", voicing: "voiced" }, airstream: "pulmonic" },
|
|
69
|
+
//lateral approximant
|
|
70
|
+
'l': { type: "consonant", features: { manner: "lateral approximant", place: "alveolar", voicing: "voiced" }, airstream: "pulmonic" },
|
|
71
|
+
'ɭ': { type: "consonant", features: { manner: "lateral approximant", place: "retroflex", voicing: "voiced" }, airstream: "pulmonic" },
|
|
72
|
+
'ʎ': { type: "consonant", features: { manner: "lateral approximant", place: "palatal", voicing: "voiced" }, airstream: "pulmonic" },
|
|
73
|
+
'ʟ': { type: "consonant", features: { manner: "lateral approximant", place: "velar", voicing: "voiced" }, airstream: "pulmonic" },
|
|
74
|
+
//non-pulmonic
|
|
75
|
+
//implosive
|
|
76
|
+
'ɓ': { type: "consonant", features: { manner: "plosive", place: "bilabial", voicing: "voiced" }, airstream: "glottalic ingressive" },
|
|
77
|
+
'ɗ': { type: "consonant", features: { manner: "plosive", place: "alveolar", voicing: "voiced" }, airstream: "glottalic ingressive" },
|
|
78
|
+
'ʄ': { type: "consonant", features: { manner: "plosive", place: "palatal", voicing: "voiced" }, airstream: "glottalic ingressive" },
|
|
79
|
+
'ɠ': { type: "consonant", features: { manner: "plosive", place: "velar", voicing: "voiced" }, airstream: "glottalic ingressive" },
|
|
80
|
+
'ʛ': { type: "consonant", features: { manner: "plosive", place: "uvular", voicing: "voiced" }, airstream: "glottalic ingressive" },
|
|
81
|
+
//clicks
|
|
82
|
+
'ʘ': { type: "consonant", features: { manner: "click", place: "bilabial" }, airstream: "lingual ingressive" },
|
|
83
|
+
'ǀ': { type: "consonant", features: { manner: "click", place: "dental" }, airstream: "lingual ingressive" },
|
|
84
|
+
'ǃ': { type: "consonant", features: { manner: "click", place: "postalveolar" }, airstream: "lingual ingressive" },
|
|
85
|
+
'ǂ': { type: "consonant", features: { manner: "click", place: "palatoalveolar" }, airstream: "lingual ingressive" },
|
|
86
|
+
'ǁ': { type: "consonant", features: { manner: "click", place: "alveolar lateral" }, airstream: "lingual ingressive" },
|
|
87
|
+
//others
|
|
88
|
+
'ʍ': { type: "consonant", features: { manner: "fricative", place: "labial-velar", voicing: "voiceless" }, airstream: "pulmonic" },
|
|
89
|
+
'ɕ': { type: "consonant", features: { manner: "fricative", place: "alveolo-palatal", voicing: "voiceless" }, airstream: "pulmonic" },
|
|
90
|
+
'ʑ': { type: "consonant", features: { manner: "fricative", place: "alveolo-palatal", voicing: "voiced" }, airstream: "pulmonic" },
|
|
91
|
+
'w': { type: "consonant", features: { manner: "approximant", place: "labial-velar", voicing: "voiced" }, airstream: "pulmonic" },
|
|
92
|
+
'ɺ': { type: "consonant", features: { manner: "lateral flap", place: "alveolar", voicing: "voiced" }, airstream: "pulmonic" },
|
|
93
|
+
'ɥ': { type: "consonant", features: { manner: "approximant", place: "labial-palatal", voicing: "voiced" }, airstream: "pulmonic" },
|
|
94
|
+
'ʜ': { type: "consonant", features: { manner: "fricative", place: "epiglottal", voicing: "voiceless" }, airstream: "pulmonic" },
|
|
95
|
+
'ʢ': { type: "consonant", features: { manner: "fricative/approximant", place: "epiglottal", voicing: "voiced" }, airstream: "pulmonic" },
|
|
96
|
+
'ʡ': { type: "consonant", features: { manner: "plosive", place: "epiglottal", voicing: "voiceless" }, airstream: "pulmonic" },
|
|
97
|
+
//coarticulated
|
|
98
|
+
'ɧ': { type: "consonant", coarticulated: true, components: ["ʃ", "x"] },
|
|
99
|
+
//vowels
|
|
100
|
+
//front
|
|
101
|
+
'i': { type: "vowel", features: { height: "close", backness: "front", rounding: "unrounded" }, airstream: "pulmonic" },
|
|
102
|
+
'y': { type: "vowel", features: { height: "close", backness: "front", rounding: "rounded" }, airstream: "pulmonic" },
|
|
103
|
+
'ɪ': { type: "vowel", features: { height: "near-close", backness: "front", rounding: "unrounded" }, airstream: "pulmonic" },
|
|
104
|
+
'ʏ': { type: "vowel", features: { height: "near-close", backness: "front", rounding: "rounded" }, airstream: "pulmonic" },
|
|
105
|
+
'e': { type: "vowel", features: { height: "close-mid", backness: "front", rounding: "unrounded" }, airstream: "pulmonic" },
|
|
106
|
+
'ø': { type: "vowel", features: { height: "close-mid", backness: "front", rounding: "rounded" }, airstream: "pulmonic" },
|
|
107
|
+
'ɛ': { type: "vowel", features: { height: "open-mid", backness: "front", rounding: "unrounded" }, airstream: "pulmonic" },
|
|
108
|
+
'œ': { type: "vowel", features: { height: "open-mid", backness: "front", rounding: "rounded" }, airstream: "pulmonic" },
|
|
109
|
+
'æ': { type: "vowel", features: { height: "near-open", backness: "front", rounding: "unrounded" }, airstream: "pulmonic" },
|
|
110
|
+
//central
|
|
111
|
+
'ə': { type: "vowel", features: { height: "mid", backness: "central", rounding: "unrounded" }, airstream: "pulmonic" },
|
|
112
|
+
'ɨ': { type: "vowel", features: { height: "close", backness: "central", rounding: "unrounded" }, airstream: "pulmonic" },
|
|
113
|
+
'ʉ': { type: "vowel", features: { height: "close", backness: "central", rounding: "rounded" }, airstream: "pulmonic" },
|
|
114
|
+
'ɘ': { type: "vowel", features: { height: "close-mid", backness: "central", rounding: "unrounded" }, airstream: "pulmonic" },
|
|
115
|
+
'ɵ': { type: "vowel", features: { height: "close-mid", backness: "central", rounding: "rounded" }, airstream: "pulmonic" },
|
|
116
|
+
'ɜ': { type: "vowel", features: { height: "open-mid", backness: "central", rounding: "unrounded" }, airstream: "pulmonic" },
|
|
117
|
+
'ɞ': { type: "vowel", features: { height: "open-mid", backness: "central", rounding: "rounded" }, airstream: "pulmonic" },
|
|
118
|
+
'ɐ': { type: "vowel", features: { height: "near-open", backness: "central", rounding: "unrounded" }, airstream: "pulmonic" },
|
|
119
|
+
//back
|
|
120
|
+
'ɯ': { type: "vowel", features: { height: "close", backness: "back", rounding: "unrounded" }, airstream: "pulmonic" },
|
|
121
|
+
'u': { type: "vowel", features: { height: "close", backness: "back", rounding: "rounded" }, airstream: "pulmonic" },
|
|
122
|
+
'ʊ': { type: "vowel", features: { height: "near-close", backness: "back", rounding: "rounded" }, airstream: "pulmonic" },
|
|
123
|
+
'ɤ': { type: "vowel", features: { height: "close-mid", backness: "back", rounding: "unrounded" }, airstream: "pulmonic" },
|
|
124
|
+
'o': { type: "vowel", features: { height: "close-mid", backness: "back", rounding: "rounded" }, airstream: "pulmonic" },
|
|
125
|
+
'ɔ': { type: "vowel", features: { height: "open-mid", backness: "back", rounding: "rounded" }, airstream: "pulmonic" },
|
|
126
|
+
'ɑ': { type: "vowel", features: { height: "open", backness: "back", rounding: "unrounded" }, airstream: "pulmonic" },
|
|
127
|
+
'ɒ': { type: "vowel", features: { height: "open", backness: "back", rounding: "rounded" }, airstream: "pulmonic" }
|
|
128
|
+
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export default core
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// generate-config.js
|
|
2
|
+
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
import core from './core.js';
|
|
7
|
+
import modifiers from './modifiers.js';
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
// -----------------------------
|
|
11
|
+
// GROUP IPA SYMBOLS BY TYPE
|
|
12
|
+
// -----------------------------
|
|
13
|
+
function groupByType(core) {
|
|
14
|
+
const groups = {};
|
|
15
|
+
|
|
16
|
+
for (const [symbol, data] of Object.entries(core)) {
|
|
17
|
+
const type = data.type || 'other';
|
|
18
|
+
if (!groups[type]) groups[type] = [];
|
|
19
|
+
groups[type].push(symbol);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return groups;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
// -----------------------------
|
|
27
|
+
// WRAP SYMBOL LINES
|
|
28
|
+
// -----------------------------
|
|
29
|
+
function wrapSymbols(symbols, perLine = 12) {
|
|
30
|
+
const lines = [];
|
|
31
|
+
for (let i = 0; i < symbols.length; i += perLine) {
|
|
32
|
+
lines.push('// ' + symbols.slice(i, i + perLine).join(' '));
|
|
33
|
+
}
|
|
34
|
+
return lines.join('\n');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
// -----------------------------
|
|
39
|
+
// IPA SECTION
|
|
40
|
+
// -----------------------------
|
|
41
|
+
function generateIPASection(core) {
|
|
42
|
+
const grouped = groupByType(core);
|
|
43
|
+
|
|
44
|
+
const sections = Object.entries(grouped).map(([type, symbols]) => {
|
|
45
|
+
symbols.sort((a, b) => a.localeCompare(b));
|
|
46
|
+
|
|
47
|
+
return [
|
|
48
|
+
`// ${type.toUpperCase()}`,
|
|
49
|
+
wrapSymbols(symbols),
|
|
50
|
+
''
|
|
51
|
+
].join('\n');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
return [
|
|
55
|
+
'/**',
|
|
56
|
+
' * IPA SYMBOL REFERENCE',
|
|
57
|
+
' * Copy/paste symbols as needed',
|
|
58
|
+
' */',
|
|
59
|
+
'',
|
|
60
|
+
...sections
|
|
61
|
+
].join('\n');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
// -----------------------------
|
|
66
|
+
// WRAP MODIFIERS HORIZONTALLY
|
|
67
|
+
// -----------------------------
|
|
68
|
+
function wrapModifiers(modList, maxLineLength = 90) {
|
|
69
|
+
const lines = [];
|
|
70
|
+
let currentLine = '// ';
|
|
71
|
+
|
|
72
|
+
modList.forEach((mod, index) => {
|
|
73
|
+
const separator = index === 0 ? '' : ' | ';
|
|
74
|
+
const nextChunk = separator + mod;
|
|
75
|
+
|
|
76
|
+
if ((currentLine + nextChunk).length > maxLineLength) {
|
|
77
|
+
lines.push(currentLine);
|
|
78
|
+
currentLine = '// ' + mod;
|
|
79
|
+
} else {
|
|
80
|
+
currentLine += nextChunk;
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
if (currentLine.trim()) lines.push(currentLine);
|
|
85
|
+
|
|
86
|
+
return lines.join('\n');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
// -----------------------------
|
|
91
|
+
// MODIFIER SECTION
|
|
92
|
+
// -----------------------------
|
|
93
|
+
function generateModifierSection(modifiers) {
|
|
94
|
+
const entries = Object.entries(modifiers);
|
|
95
|
+
entries.sort(([a], [b]) => a.localeCompare(b));
|
|
96
|
+
|
|
97
|
+
const formatted = entries.map(([key, data]) => {
|
|
98
|
+
const appliesTo = Array.isArray(data.appliesTo)
|
|
99
|
+
? data.appliesTo.join(', ')
|
|
100
|
+
: data.appliesTo || '';
|
|
101
|
+
return `${key}(${appliesTo})`;
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
return [
|
|
105
|
+
'/**',
|
|
106
|
+
' * MODIFIER REFERENCE',
|
|
107
|
+
' * modifier(appliesTo)',
|
|
108
|
+
' */',
|
|
109
|
+
'',
|
|
110
|
+
wrapModifiers(formatted),
|
|
111
|
+
''
|
|
112
|
+
].join('\n');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
// -----------------------------
|
|
117
|
+
// ORTHOGRAPHY EXPORT SECTION
|
|
118
|
+
// -----------------------------
|
|
119
|
+
function generateOrthographyExport() {
|
|
120
|
+
const commentedOrthography = [
|
|
121
|
+
'// k: ["k", ["aspirated"]],',
|
|
122
|
+
'// a: ["ɑ", ["more_rounded"]],',
|
|
123
|
+
'// x̱: [',
|
|
124
|
+
'// ["k", ["ejective", "aspirated"]],',
|
|
125
|
+
'// ["x", []]',
|
|
126
|
+
'// ],',
|
|
127
|
+
'// l: ["ɬ"]'
|
|
128
|
+
].join('\n');
|
|
129
|
+
|
|
130
|
+
return [
|
|
131
|
+
'/**',
|
|
132
|
+
' * ORTHOGRAPHY CONFIG',
|
|
133
|
+
' * Use parseConfig() at runtime',
|
|
134
|
+
' *',
|
|
135
|
+
' * Fill in your orthography below; examples are commented out.',
|
|
136
|
+
' */',
|
|
137
|
+
'',
|
|
138
|
+
"import { parseConfig } from './parser.js';",
|
|
139
|
+
'',
|
|
140
|
+
'const orthography = {',
|
|
141
|
+
commentedOrthography,
|
|
142
|
+
'};',
|
|
143
|
+
'',
|
|
144
|
+
'export default parseConfig(orthography);',
|
|
145
|
+
''
|
|
146
|
+
].join('\n');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
// -----------------------------
|
|
151
|
+
// MAIN GENERATOR
|
|
152
|
+
// -----------------------------
|
|
153
|
+
function generateConfig() {
|
|
154
|
+
const content = [
|
|
155
|
+
generateIPASection(core),
|
|
156
|
+
'',
|
|
157
|
+
generateModifierSection(modifiers),
|
|
158
|
+
'',
|
|
159
|
+
generateOrthographyExport()
|
|
160
|
+
].join('\n');
|
|
161
|
+
|
|
162
|
+
const outputPath = path.join(process.cwd(), 'ipa.config.js');
|
|
163
|
+
fs.writeFileSync(outputPath, content, 'utf-8');
|
|
164
|
+
|
|
165
|
+
console.log('✅ ipa.config.js generated with parseConfig() export');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
// RUN
|
|
170
|
+
generateConfig();
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import readline from "readline";
|
|
4
|
+
import core from "./core.js";
|
|
5
|
+
import modifiers from "./modifiers.js";
|
|
6
|
+
|
|
7
|
+
/** Pick random element */
|
|
8
|
+
const pickRandom = (arr) => arr[Math.floor(Math.random() * arr.length)];
|
|
9
|
+
|
|
10
|
+
/** Shuffle array */
|
|
11
|
+
const shuffle = (arr) => {
|
|
12
|
+
for (let i = arr.length - 1; i > 0; i--) {
|
|
13
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
14
|
+
[arr[i], arr[j]] = [arr[j], arr[i]];
|
|
15
|
+
}
|
|
16
|
+
return arr;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/** Prompt helper */
|
|
20
|
+
function prompt(query) {
|
|
21
|
+
const rl = readline.createInterface({
|
|
22
|
+
input: process.stdin,
|
|
23
|
+
output: process.stdout,
|
|
24
|
+
});
|
|
25
|
+
return new Promise((resolve) =>
|
|
26
|
+
rl.question(query, (ans) => {
|
|
27
|
+
rl.close();
|
|
28
|
+
resolve(ans);
|
|
29
|
+
})
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Generate conlang orthography */
|
|
34
|
+
function generateConlang({ alphabet, mod1Chance, mod2Chance, affricateChance }) {
|
|
35
|
+
const letters = alphabet.split("");
|
|
36
|
+
const usedIPA = new Set();
|
|
37
|
+
const orthography = {};
|
|
38
|
+
const coreKeys = shuffle(Object.keys(core));
|
|
39
|
+
|
|
40
|
+
letters.forEach((letter) => {
|
|
41
|
+
// Pick unused IPA
|
|
42
|
+
let ipaKey;
|
|
43
|
+
while (true) {
|
|
44
|
+
const candidate = pickRandom(coreKeys);
|
|
45
|
+
if (!usedIPA.has(candidate)) {
|
|
46
|
+
ipaKey = candidate;
|
|
47
|
+
usedIPA.add(candidate);
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const baseObj = core[ipaKey];
|
|
53
|
+
const modifiersList = [];
|
|
54
|
+
|
|
55
|
+
// Modifier 1
|
|
56
|
+
if (Math.random() < mod1Chance) {
|
|
57
|
+
const compatible = Object.entries(modifiers).filter(
|
|
58
|
+
([, mod]) => !mod.appliesTo || mod.appliesTo.includes(baseObj?.type)
|
|
59
|
+
);
|
|
60
|
+
if (compatible.length) modifiersList.push(pickRandom(compatible)[0]);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Modifier 2
|
|
64
|
+
if (Math.random() < mod2Chance && modifiersList.length) {
|
|
65
|
+
const compatible = Object.entries(modifiers).filter(
|
|
66
|
+
([key, mod]) =>
|
|
67
|
+
!modifiersList.includes(key) &&
|
|
68
|
+
(!mod.appliesTo || mod.appliesTo.includes(baseObj?.type))
|
|
69
|
+
);
|
|
70
|
+
if (compatible.length) modifiersList.push(pickRandom(compatible)[0]);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Plosive → affricate
|
|
74
|
+
if (baseObj?.features?.manner === "plosive" && Math.random() < affricateChance) {
|
|
75
|
+
const fricatives = Object.entries(core).filter(
|
|
76
|
+
([, obj]) =>
|
|
77
|
+
obj?.features?.manner === "fricative" &&
|
|
78
|
+
obj.features.place === baseObj.features.place
|
|
79
|
+
);
|
|
80
|
+
if (fricatives.length) {
|
|
81
|
+
const fricKey = pickRandom(fricatives)[0];
|
|
82
|
+
orthography[letter] = [
|
|
83
|
+
[ipaKey, modifiersList],
|
|
84
|
+
[fricKey, []],
|
|
85
|
+
];
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
orthography[letter] = [ipaKey, modifiersList];
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
return orthography;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Main async function */
|
|
97
|
+
async function main() {
|
|
98
|
+
const alphabet = (await prompt("Enter alphabet (default a-z): ")) || "abcdefghijklmnopqrstuvwxyz";
|
|
99
|
+
const mod1 = parseFloat((await prompt("Modifier 1 chance (default 0.33): ")) || "0.33");
|
|
100
|
+
const mod2 = parseFloat((await prompt("Modifier 2 chance (default 0.06): ")) || "0.06");
|
|
101
|
+
const aff = parseFloat((await prompt("Plosive → affricate chance (default 0.1): ")) || "0.1");
|
|
102
|
+
|
|
103
|
+
const orthography = generateConlang({
|
|
104
|
+
alphabet,
|
|
105
|
+
mod1Chance: mod1,
|
|
106
|
+
mod2Chance: mod2,
|
|
107
|
+
affricateChance: aff,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const outPath = path.join(process.cwd(), "generatedConlang.js");
|
|
111
|
+
const lines = Object.entries(orthography).map(
|
|
112
|
+
([l, v]) => ` ${JSON.stringify(l)}:${JSON.stringify(v)}`
|
|
113
|
+
);
|
|
114
|
+
const content = `/** Generated conlang orthography */\nconst generatedOrthography={\n${lines.join(",\n")}\n};\nexport default generatedOrthography;`;
|
|
115
|
+
|
|
116
|
+
fs.writeFileSync(outPath, content, "utf-8");
|
|
117
|
+
console.log("✅ generatedConlang.js written");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
main();
|
package/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Import your data layers
|
|
2
|
+
import core from "./core.js"; // base consonants & vowels
|
|
3
|
+
import modifiers from "./modifiers.js"; // diacritics, tones, suprasegmentals
|
|
4
|
+
import rules from "./rules.js"; // modifier compatibility rules
|
|
5
|
+
import parser from "./parser.js"; // parser (parsePhoneme, parseUnit, parseConfig)
|
|
6
|
+
|
|
7
|
+
// Export the layers so users can access them if needed
|
|
8
|
+
export const { parsePhoneme, parseUnit, parseConfig } = parser;
|
|
9
|
+
export { core, modifiers, rules, parser };
|
package/modifiers.js
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
const modifiers = {
|
|
2
|
+
// DIACRITICS
|
|
3
|
+
|
|
4
|
+
voiceless: {
|
|
5
|
+
symbol: "̥",
|
|
6
|
+
appliesTo: "consonant",
|
|
7
|
+
effects: { voicing: "voiceless" }
|
|
8
|
+
},
|
|
9
|
+
voiced: {
|
|
10
|
+
symbol: "̬",
|
|
11
|
+
appliesTo: "consonant",
|
|
12
|
+
effects: { voicing: "voiced" }
|
|
13
|
+
},
|
|
14
|
+
aspirated: {
|
|
15
|
+
symbol: "ʰ",
|
|
16
|
+
appliesTo: "consonant",
|
|
17
|
+
effects: { aspiration: true }
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
more_rounded: {
|
|
21
|
+
symbol: "̹",
|
|
22
|
+
appliesTo: "vowel",
|
|
23
|
+
effects: { rounding: "more" }
|
|
24
|
+
},
|
|
25
|
+
less_rounded: {
|
|
26
|
+
symbol: "̜",
|
|
27
|
+
appliesTo: "vowel",
|
|
28
|
+
effects: { rounding: "less" }
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
advanced: {
|
|
32
|
+
symbol: "̟",
|
|
33
|
+
appliesTo: "consonant",
|
|
34
|
+
effects: { placeModifier: "advanced" }
|
|
35
|
+
},
|
|
36
|
+
retracted: {
|
|
37
|
+
symbol: "̠",
|
|
38
|
+
appliesTo: "consonant",
|
|
39
|
+
effects: { placeModifier: "retracted" }
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
centralized: {
|
|
43
|
+
symbol: "̈",
|
|
44
|
+
appliesTo: "vowel",
|
|
45
|
+
effects: { backness: "centralized" }
|
|
46
|
+
},
|
|
47
|
+
mid_centralized: {
|
|
48
|
+
symbol: "̽",
|
|
49
|
+
appliesTo: "vowel",
|
|
50
|
+
effects: { backness: "mid-centralized" }
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
syllabic: {
|
|
54
|
+
symbol: "̩",
|
|
55
|
+
appliesTo: "consonant",
|
|
56
|
+
effects: { syllabic: true }
|
|
57
|
+
},
|
|
58
|
+
non_syllabic: {
|
|
59
|
+
symbol: "̯",
|
|
60
|
+
appliesTo: "vowel",
|
|
61
|
+
effects: { syllabic: false }
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
dental: {
|
|
65
|
+
symbol: "̪",
|
|
66
|
+
appliesTo: "consonant",
|
|
67
|
+
effects: { placeModifier: "dental" }
|
|
68
|
+
},
|
|
69
|
+
apical: {
|
|
70
|
+
symbol: "̺",
|
|
71
|
+
appliesTo: "consonant",
|
|
72
|
+
effects: { articulation: "apical" }
|
|
73
|
+
},
|
|
74
|
+
laminal: {
|
|
75
|
+
symbol: "̻",
|
|
76
|
+
appliesTo: "consonant",
|
|
77
|
+
effects: { articulation: "laminal" }
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
velarized_or_pharyngealized: {
|
|
81
|
+
symbol: "ˠ",
|
|
82
|
+
appliesTo: "consonant",
|
|
83
|
+
effects: { secondaryArticulation: "velarized_or_pharyngealized" }
|
|
84
|
+
},
|
|
85
|
+
velarized: {
|
|
86
|
+
symbol: "̴",
|
|
87
|
+
appliesTo: "consonant",
|
|
88
|
+
effects: { secondaryArticulation: "velarized" }
|
|
89
|
+
},
|
|
90
|
+
palatalized: {
|
|
91
|
+
symbol: "ʲ",
|
|
92
|
+
appliesTo: "consonant",
|
|
93
|
+
effects: { secondaryArticulation: "palatalized" }
|
|
94
|
+
},
|
|
95
|
+
labialized: {
|
|
96
|
+
symbol: "ʷ",
|
|
97
|
+
appliesTo: "consonant",
|
|
98
|
+
effects: { secondaryArticulation: "labialized" }
|
|
99
|
+
},
|
|
100
|
+
pharyngealized: {
|
|
101
|
+
symbol: "ˤ",
|
|
102
|
+
appliesTo: "consonant",
|
|
103
|
+
effects: { secondaryArticulation: "pharyngealized" }
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
nasalized: {
|
|
107
|
+
symbol: "̃",
|
|
108
|
+
appliesTo: ["vowel", "consonant"],
|
|
109
|
+
effects: { nasalization: true }
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
rhoticity: {
|
|
113
|
+
symbol: "˞",
|
|
114
|
+
appliesTo: "vowel",
|
|
115
|
+
effects: { rhotic: true }
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
length_half_long: {
|
|
119
|
+
symbol: "ˑ",
|
|
120
|
+
appliesTo: ["vowel", "consonant"],
|
|
121
|
+
effects: { length: "half-long" }
|
|
122
|
+
},
|
|
123
|
+
length_long: {
|
|
124
|
+
symbol: "ː",
|
|
125
|
+
appliesTo: ["vowel", "consonant"],
|
|
126
|
+
effects: { length: "long" }
|
|
127
|
+
},
|
|
128
|
+
extra_short: {
|
|
129
|
+
symbol: "̆",
|
|
130
|
+
appliesTo: ["vowel", "consonant"],
|
|
131
|
+
effects: { length: "extra-short" }
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
voiceless_flap: {
|
|
135
|
+
symbol: "̥̆",
|
|
136
|
+
appliesTo: "consonant",
|
|
137
|
+
effects: {
|
|
138
|
+
voicing: "voiceless",
|
|
139
|
+
mannerModifier: "flap"
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
creaky_voice: {
|
|
144
|
+
symbol: "̰",
|
|
145
|
+
appliesTo: ["vowel", "consonant"],
|
|
146
|
+
effects: { phonation: "creaky" }
|
|
147
|
+
},
|
|
148
|
+
breathy_voice: {
|
|
149
|
+
symbol: "̤",
|
|
150
|
+
appliesTo: ["vowel", "consonant"],
|
|
151
|
+
effects: { phonation: "breathy" }
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
linguolabial: {
|
|
155
|
+
symbol: "̼",
|
|
156
|
+
appliesTo: "consonant",
|
|
157
|
+
effects: { placeModifier: "linguolabial" }
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
raised: {
|
|
161
|
+
symbol: "̝",
|
|
162
|
+
appliesTo: ["vowel", "consonant"],
|
|
163
|
+
effects: { heightModifier: "raised" }
|
|
164
|
+
},
|
|
165
|
+
lowered: {
|
|
166
|
+
symbol: "̞",
|
|
167
|
+
appliesTo: ["vowel", "consonant"],
|
|
168
|
+
effects: { heightModifier: "lowered" }
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
advanced_tongue_root: {
|
|
172
|
+
symbol: "̘",
|
|
173
|
+
appliesTo: "vowel",
|
|
174
|
+
effects: { tongueRoot: "advanced" }
|
|
175
|
+
},
|
|
176
|
+
retracted_tongue_root: {
|
|
177
|
+
symbol: "̙",
|
|
178
|
+
appliesTo: "vowel",
|
|
179
|
+
effects: { tongueRoot: "retracted" }
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
// TONES / ACCENTS
|
|
183
|
+
|
|
184
|
+
high_tone: {
|
|
185
|
+
symbol: "́",
|
|
186
|
+
appliesTo: ["vowel", "consonant"],
|
|
187
|
+
effects: { tone: "high" }
|
|
188
|
+
},
|
|
189
|
+
low_tone: {
|
|
190
|
+
symbol: "̀",
|
|
191
|
+
appliesTo: ["vowel", "consonant"],
|
|
192
|
+
effects: { tone: "low" }
|
|
193
|
+
},
|
|
194
|
+
rising_tone: {
|
|
195
|
+
symbol: "̌",
|
|
196
|
+
appliesTo: ["vowel", "consonant"],
|
|
197
|
+
effects: { tone: "rising" }
|
|
198
|
+
},
|
|
199
|
+
falling_tone: {
|
|
200
|
+
symbol: "̂",
|
|
201
|
+
appliesTo: ["vowel", "consonant"],
|
|
202
|
+
effects: { tone: "falling" }
|
|
203
|
+
},
|
|
204
|
+
extra_high_tone: {
|
|
205
|
+
symbol: "᷄",
|
|
206
|
+
appliesTo: ["vowel", "consonant"],
|
|
207
|
+
effects: { tone: "extra-high" }
|
|
208
|
+
},
|
|
209
|
+
extra_low_tone: {
|
|
210
|
+
symbol: "᷅",
|
|
211
|
+
appliesTo: ["vowel", "consonant"],
|
|
212
|
+
effects: { tone: "extra-low" }
|
|
213
|
+
},
|
|
214
|
+
|
|
215
|
+
extra_high_pitch: {
|
|
216
|
+
symbol: "᷄",
|
|
217
|
+
appliesTo: ["vowel", "consonant"],
|
|
218
|
+
effects: { pitch: "extra-high" }
|
|
219
|
+
},
|
|
220
|
+
extra_low_pitch: {
|
|
221
|
+
symbol: "᷅",
|
|
222
|
+
appliesTo: ["vowel", "consonant"],
|
|
223
|
+
effects: { pitch: "extra-low" }
|
|
224
|
+
},
|
|
225
|
+
|
|
226
|
+
downstep: {
|
|
227
|
+
symbol: "ꜜ",
|
|
228
|
+
appliesTo: ["vowel", "consonant"],
|
|
229
|
+
effects: { toneModifier: "downstep" }
|
|
230
|
+
},
|
|
231
|
+
upstep: {
|
|
232
|
+
symbol: "ꜛ",
|
|
233
|
+
appliesTo: ["vowel", "consonant"],
|
|
234
|
+
effects: { toneModifier: "upstep" }
|
|
235
|
+
},
|
|
236
|
+
|
|
237
|
+
no_audible_release: {
|
|
238
|
+
symbol: "̚",
|
|
239
|
+
appliesTo: "consonant",
|
|
240
|
+
effects: { release: "none" }
|
|
241
|
+
},
|
|
242
|
+
//ejective
|
|
243
|
+
ejective: {
|
|
244
|
+
symbol: "ʼ",
|
|
245
|
+
appliesTo: "consonant",
|
|
246
|
+
effects: { airstream: "egressive_glottalic" } // or just mark it as ejective
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
export default modifiers;
|
package/package.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ipa-core",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Language-agnostic IPA core with features",
|
|
5
|
+
"license": "ISC",
|
|
6
|
+
"author": "Cody Bruno",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "index.js",
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "node test.js",
|
|
11
|
+
"init:ipa": "node generate-config.js",
|
|
12
|
+
"make-conlang": "node generate-conlang.js"
|
|
13
|
+
},
|
|
14
|
+
"keywords": ["ipa", "phoneme", "language", "linguistics"]
|
|
15
|
+
}
|
package/parser.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// parser.js
|
|
2
|
+
|
|
3
|
+
import core from "./core";
|
|
4
|
+
import modifiers from "./modifiers";
|
|
5
|
+
import rules from "./rules";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Parses a single phoneme with optional modifiers.
|
|
9
|
+
* @param {string} base - Base IPA symbol
|
|
10
|
+
* @param {string[]} appliedModifiers - List of modifier keys
|
|
11
|
+
* @returns {object} - Full feature object
|
|
12
|
+
*/
|
|
13
|
+
function parsePhoneme(base, appliedModifiers = []) {
|
|
14
|
+
const baseObj = core[base];
|
|
15
|
+
if (!baseObj) {
|
|
16
|
+
throw new Error(`Unknown base phoneme: ${base}`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let features = { ...baseObj.features };
|
|
20
|
+
const modifiersApplied = [];
|
|
21
|
+
|
|
22
|
+
for (const modKey of appliedModifiers) {
|
|
23
|
+
const mod = modifiers[modKey];
|
|
24
|
+
if (!mod) {
|
|
25
|
+
throw new Error(`Unknown modifier: ${modKey}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const incompatible = rules[modKey]?.incompatibleWith || [];
|
|
29
|
+
const conflict = modifiersApplied.find(applied => incompatible.includes(applied));
|
|
30
|
+
if (conflict) {
|
|
31
|
+
console.warn(`Modifier "${modKey}" is incompatible with already applied "${conflict}". Skipping.`);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (mod.features) {
|
|
36
|
+
features = { ...features, ...mod.features };
|
|
37
|
+
} else {
|
|
38
|
+
features[modKey] = true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
modifiersApplied.push(modKey);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
base,
|
|
46
|
+
symbol: baseObj.symbol || base,
|
|
47
|
+
features,
|
|
48
|
+
modifiersApplied
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Helper to create a phoneme unit for orthography config.
|
|
54
|
+
* Can be a single phoneme or a sequence object.
|
|
55
|
+
* @param {string|object} unit - Single base or a sequence object
|
|
56
|
+
* @param {string[]} appliedModifiers - Modifiers for single phoneme (ignored if sequence)
|
|
57
|
+
* @returns {object}
|
|
58
|
+
*/
|
|
59
|
+
function parseUnit(unit, appliedModifiers = []) {
|
|
60
|
+
// if unit is a string, treat as single phoneme
|
|
61
|
+
if (typeof unit === "string") {
|
|
62
|
+
return { type: "single", base: unit, modifiers: appliedModifiers };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// if unit is an object with multiple phonemes, treat as sequence
|
|
66
|
+
if (typeof unit === "object") {
|
|
67
|
+
const sequence = [];
|
|
68
|
+
for (const base in unit) {
|
|
69
|
+
const mods = unit[base] || [];
|
|
70
|
+
sequence.push({ base, modifiers: mods });
|
|
71
|
+
}
|
|
72
|
+
return { type: "sequence", sequence };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
throw new Error(`Invalid unit: ${unit}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Parses a unit (single or sequence) into full feature objects
|
|
80
|
+
* @param {object} unitObj
|
|
81
|
+
* @returns {object|array} - Parsed phoneme object or array of parsed phonemes
|
|
82
|
+
*/
|
|
83
|
+
function parseUnitObj(unitObj) {
|
|
84
|
+
if (unitObj.type === "single") {
|
|
85
|
+
return parsePhoneme(unitObj.base, unitObj.modifiers);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (unitObj.type === "sequence") {
|
|
89
|
+
return unitObj.sequence.map(u => parsePhoneme(u.base, u.modifiers));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
throw new Error(`Unknown unit type: ${unitObj.type}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Parses an entire orthography config.
|
|
97
|
+
* Supports:
|
|
98
|
+
* 1) Full parseUnit() objects
|
|
99
|
+
* 2) Single-phoneme shorthand: ["base", ["modifiers"]]
|
|
100
|
+
* 3) Sequence shorthand: [["base1", ["mods1"]], ["base2", ["mods2"]], ...]
|
|
101
|
+
* @param {object} config - { letter: parseUnit(...) OR shorthand }
|
|
102
|
+
* @returns {object} parsed - { letter: parsedPhoneme OR array if sequence }
|
|
103
|
+
*/
|
|
104
|
+
function parseConfig(config) {
|
|
105
|
+
const parsed = {};
|
|
106
|
+
|
|
107
|
+
for (const letter in config) {
|
|
108
|
+
const val = config[letter];
|
|
109
|
+
|
|
110
|
+
let unitObj;
|
|
111
|
+
|
|
112
|
+
// Already a parseUnit object
|
|
113
|
+
if (val && typeof val === "object" && val.type && (val.type === "single" || val.type === "sequence")) {
|
|
114
|
+
unitObj = val;
|
|
115
|
+
}
|
|
116
|
+
// Shorthand single phoneme: ["base", ["modifiers"]]
|
|
117
|
+
else if (Array.isArray(val) && typeof val[0] === "string") {
|
|
118
|
+
const base = val[0];
|
|
119
|
+
const mods = Array.isArray(val[1]) ? val[1] : [];
|
|
120
|
+
unitObj = { type: "single", base, modifiers: mods };
|
|
121
|
+
}
|
|
122
|
+
// Shorthand sequence: [["base1", ["mods1"]], ["base2", ["mods2"]], ...]
|
|
123
|
+
else if (Array.isArray(val) && val.every(el => Array.isArray(el) && typeof el[0] === "string")) {
|
|
124
|
+
const sequence = val.map(([base, mods]) => ({ base, modifiers: Array.isArray(mods) ? mods : [] }));
|
|
125
|
+
unitObj = { type: "sequence", sequence };
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
throw new Error(`Invalid orthography entry for "${letter}": ${JSON.stringify(val)}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
parsed[letter] = parseUnitObj(unitObj);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return parsed;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export { parsePhoneme, parseUnit, parseConfig };
|
package/rules.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
//modifier compatability
|
|
2
|
+
const rules = {
|
|
3
|
+
// Voicing / phonation
|
|
4
|
+
voiceless: { incompatibleWith: ["voiced", "creaky_voice", "breathy_voice"] },
|
|
5
|
+
voiced: { incompatibleWith: ["voiceless", "voiceless_flap"] },
|
|
6
|
+
voiceless_flap: { incompatibleWith: ["voiced", "aspirated"] },
|
|
7
|
+
creaky_voice: { incompatibleWith: ["voiceless"] },
|
|
8
|
+
breathy_voice: { incompatibleWith: ["voiceless"] },
|
|
9
|
+
|
|
10
|
+
// Place / articulation
|
|
11
|
+
advanced: { incompatibleWith: ["retracted", "velarized_or_pharyngealized"] },
|
|
12
|
+
retracted: { incompatibleWith: ["advanced", "velarized_or_pharyngealized"] },
|
|
13
|
+
palatalized: { incompatibleWith: ["velarized_or_pharyngealized"] },
|
|
14
|
+
velarized_or_pharyngealized: { incompatibleWith: ["palatalized", "advanced", "retracted"] },
|
|
15
|
+
pharyngealized: { incompatibleWith: ["palatalized"] },
|
|
16
|
+
linguolabial: { incompatibleWith: [] },
|
|
17
|
+
dental: { incompatibleWith: [] },
|
|
18
|
+
apical: { incompatibleWith: [] },
|
|
19
|
+
laminal: { incompatibleWith: [] },
|
|
20
|
+
velarized: { incompatibleWith: ["palatalized", "advanced", "retracted"] },
|
|
21
|
+
|
|
22
|
+
// Syllabicity
|
|
23
|
+
syllabic: { incompatibleWith: ["non_syllabic"] },
|
|
24
|
+
non_syllabic: { incompatibleWith: ["syllabic"] },
|
|
25
|
+
|
|
26
|
+
// Nasalization / rhoticity
|
|
27
|
+
nasalized: { incompatibleWith: [] },
|
|
28
|
+
rhoticity: { incompatibleWith: [] },
|
|
29
|
+
|
|
30
|
+
// Length / duration
|
|
31
|
+
extra_short: { incompatibleWith: ["length_half_long", "length_long"] },
|
|
32
|
+
length_half_long: { incompatibleWith: ["extra_short", "length_long"] },
|
|
33
|
+
length_long: { incompatibleWith: ["extra_short", "length_half_long"] },
|
|
34
|
+
|
|
35
|
+
// Segment-level tones / accents
|
|
36
|
+
high_tone: { incompatibleWith: ["low_tone", "falling_tone", "extra_low_tone"] },
|
|
37
|
+
low_tone: { incompatibleWith: ["high_tone", "rising_tone", "extra_high_tone"] },
|
|
38
|
+
rising_tone: { incompatibleWith: ["falling_tone", "low_tone"] },
|
|
39
|
+
falling_tone: { incompatibleWith: ["rising_tone", "high_tone"] },
|
|
40
|
+
extra_high_tone: { incompatibleWith: ["extra_low_tone", "low_tone"] },
|
|
41
|
+
extra_low_tone: { incompatibleWith: ["extra_high_tone", "high_tone"] },
|
|
42
|
+
downstep: { incompatibleWith: [] },
|
|
43
|
+
upstep: { incompatibleWith: [] },
|
|
44
|
+
extra_high_pitch: { incompatibleWith: ["extra_low_pitch"] },
|
|
45
|
+
extra_low_pitch: { incompatibleWith: ["extra_high_pitch"] },
|
|
46
|
+
|
|
47
|
+
// Special consonant markers
|
|
48
|
+
aspirated: { incompatibleWith: ["voiceless_flap", "no_audible_release"] },
|
|
49
|
+
no_audible_release: { incompatibleWith: ["aspirated"] }
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
module.exports = rules
|
package/test.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// test.js
|
|
2
|
+
|
|
3
|
+
const { parseUnit, parseConfig } = require("./parser");
|
|
4
|
+
|
|
5
|
+
// Sample orthography config
|
|
6
|
+
const orthography = {
|
|
7
|
+
// A single consonant with a modifier
|
|
8
|
+
k: parseUnit("k", ["aspirated"]),
|
|
9
|
+
|
|
10
|
+
// A single vowel with a modifier
|
|
11
|
+
a: parseUnit("ɑ", ["more_rounded"]),
|
|
12
|
+
|
|
13
|
+
// A consonant sequence (affricate)
|
|
14
|
+
x̱: parseUnit({
|
|
15
|
+
k: ["ejective", "aspirated"],
|
|
16
|
+
x: []
|
|
17
|
+
}),
|
|
18
|
+
|
|
19
|
+
// Another single consonant
|
|
20
|
+
l: parseUnit("ɬ")
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Another sample orthography config using shorthand
|
|
24
|
+
const shorthandOrthography = {
|
|
25
|
+
// A single consonant with a modifier
|
|
26
|
+
k: ["k", ["aspirated"]],
|
|
27
|
+
|
|
28
|
+
// A single vowel with a modifier
|
|
29
|
+
a: ["ɑ", ["more_rounded"]],
|
|
30
|
+
|
|
31
|
+
// A consonant sequence (affricate)
|
|
32
|
+
x̱: [[
|
|
33
|
+
'k', ["ejective", "aspirated"],
|
|
34
|
+
'x', []
|
|
35
|
+
]],
|
|
36
|
+
|
|
37
|
+
// Another single consonant
|
|
38
|
+
l: ["ɬ"]
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
console.log("=== Orthography Config ===");
|
|
42
|
+
console.log(JSON.stringify(orthography, null, 2));
|
|
43
|
+
|
|
44
|
+
const parsed = parseConfig(orthography);
|
|
45
|
+
console.log("\n=== Parsed Output ===");
|
|
46
|
+
console.log(JSON.stringify(parsed, null, 2));
|
|
47
|
+
|
|
48
|
+
// Optional: inspect a single phoneme
|
|
49
|
+
const singlePhoneme = parsed.k;
|
|
50
|
+
console.log("\n=== Single Phoneme 'k' Features ===");
|
|
51
|
+
console.log(singlePhoneme);
|
|
52
|
+
|
|
53
|
+
// Optional: inspect a sequence
|
|
54
|
+
const affricateSequence = parsed.x̱;
|
|
55
|
+
console.log("\n=== Affricate Sequence 'x̱' Features ===");
|
|
56
|
+
console.log(affricateSequence);
|
|
57
|
+
|
|
58
|
+
const parsedShorthand = parseConfig(shorthandOrthography);
|
|
59
|
+
console.log("\n=== Parsed Output2 ===");
|
|
60
|
+
console.log(JSON.stringify(parsedShorthand, null, 2));
|