ipa-core 1.1.13 → 1.3.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 CHANGED
Binary file
package/core.js CHANGED
@@ -95,7 +95,7 @@ const core = {
95
95
  'ʢ': { type: "consonant", features: { manner: "fricative/approximant", place: "epiglottal", voicing: "voiced" }, airstream: "pulmonic" },
96
96
  'ʡ': { type: "consonant", features: { manner: "plosive", place: "epiglottal", voicing: "voiceless" }, airstream: "pulmonic" },
97
97
  //coarticulated
98
- 'ɧ': { type: "consonant", coarticulated: true, components: ["ʃ", "x"] },
98
+ // 'ɧ': { type: "consonant", coarticulated: true, components: ["ʃ", "x"] },
99
99
  //vowels
100
100
  //front
101
101
  'i': { type: "vowel", features: { height: "close", backness: "front", rounding: "unrounded" }, airstream: "pulmonic" },
package/dictionary.js ADDED
@@ -0,0 +1,105 @@
1
+ const dictionary = {
2
+ // AIRSTREAM
3
+ pulmonic: "Use airflow from the lungs to produce the sound",
4
+ "glottalic ingressive": "Draw air inward using the glottis mechanism",
5
+ "lingual ingressive": "Draw air inward using the tongue (click mechanism)",
6
+ ejective: "Release a burst of air using a glottalic upward push",
7
+
8
+ // MANNER
9
+ plosive: "Completely block airflow, build pressure, then release it in a burst",
10
+ nasal: "Lower the velum so air flows through the nose",
11
+ trill: "Allow an articulator to vibrate rapidly in the airstream",
12
+ "tap/flap": "Make a single quick strike of an articulator against its contact point",
13
+ fricative: "Narrow the airflow channel to create friction noise",
14
+ approximant: "Shape the mouth for sound without creating turbulent airflow",
15
+ "lateral fricative": "Direct airflow along the sides of the tongue while creating friction",
16
+ "lateral approximant": "Allow air to flow smoothly along the sides of the tongue",
17
+ click: "Create a suction pocket in the mouth and release it sharply",
18
+
19
+ // PLACE
20
+ bilabial: "Bring both lips together",
21
+ labiodental: "Place lower lip against upper teeth",
22
+ dental: "Place the tongue against the teeth",
23
+ alveolar: "Place the tongue at the alveolar ridge just behind the teeth",
24
+ postalveolar: "Place the tongue just behind the alveolar ridge",
25
+ retroflex: "Curl the tongue tip backward toward the palate",
26
+ palatal: "Raise the middle of the tongue to the hard palate",
27
+ velar: "Raise the back of the tongue to the soft palate",
28
+ uvular: "Raise the back of the tongue toward the uvula",
29
+ pharyngeal: "Constrict the throat in the pharynx area",
30
+ glottal: "Use the vocal folds as the place of articulation",
31
+ epiglottal: "Use the epiglottis to constrict airflow",
32
+
33
+ // COMPLEX PLACE TYPES
34
+ "alveolo-palatal": "Position the tongue between the alveolar ridge and hard palate",
35
+ "labial-velar": "Simultaneously use the lips and the soft palate",
36
+
37
+ // VOICING
38
+ voiceless: "Do not vibrate the vocal cords",
39
+ voiced: "Vibrate the vocal cords",
40
+
41
+ // VOWEL HEIGHT
42
+ close: "Raise the tongue high in the mouth",
43
+ "near-close": "Raise the tongue slightly lower than close vowels",
44
+ "close-mid": "Position the tongue between close and mid height",
45
+ mid: "Keep the tongue in a neutral height position",
46
+ "open-mid": "Lower the tongue between mid and open",
47
+ "near-open": "Lower the tongue slightly above fully open",
48
+ open: "Lower the tongue as far as possible",
49
+
50
+ // VOWEL BACKNESS
51
+ front: "Position the tongue toward the front of the mouth",
52
+ central: "Position the tongue in the center of the mouth",
53
+ back: "Position the tongue toward the back of the mouth",
54
+
55
+ // ROUNDING
56
+ rounded: "Round the lips",
57
+ unrounded: "Keep the lips relaxed and unrounded",
58
+
59
+ // LENGTH
60
+ "extra-short": "Produce the sound with very short duration",
61
+ "half-long": "Hold the sound slightly longer than normal",
62
+ long: "Hold the sound for an extended duration",
63
+
64
+ // TONE
65
+ high: "Produce the sound with a high pitch",
66
+ low: "Produce the sound with a low pitch",
67
+ rising: "Start low and move to a higher pitch",
68
+ falling: "Start high and move to a lower pitch",
69
+ "extra-high": "Produce the sound with a very high pitch",
70
+ "extra-low": "Produce the sound with a very low pitch",
71
+
72
+ // TONE MODIFIERS
73
+ downstep: "Lower pitch relative to the previous tone",
74
+ upstep: "Raise pitch relative to the previous tone",
75
+
76
+ // RELEASE
77
+ none: "Do not release the closure audibly",
78
+
79
+ // SECONDARY ARTICULATION
80
+ aspirated: "Release the sound with a strong burst of air",
81
+ palatalized: "Raise the tongue toward the hard palate during articulation",
82
+ velarized: "Raise the back of the tongue toward the soft palate",
83
+ labialized: "Round the lips during articulation",
84
+ pharyngealized: "Constrict the pharynx during articulation",
85
+
86
+ // PHONATION
87
+ creaky_voice: "Use irregular vocal fold vibration",
88
+ breathy_voice: "Allow extra airflow through loosely vibrating vocal folds",
89
+
90
+ // NASALIZATION / RHOTICITY
91
+ nasalized: "Lower the velum so air escapes through the nose during articulation",
92
+ rhoticity: "Add an r-colored quality to the sound",
93
+
94
+ // CONSONANT/VOWEL TYPES
95
+ consonant: "Produce a sound with significant constriction of airflow",
96
+ vowel: "Produce a sound without significant obstruction of airflow",
97
+
98
+ // SEQUENCE TYPES
99
+ affricate: "Begin with a complete stop, then release into a fricative",
100
+ diphthong: "Move smoothly from one vowel position to another within a single syllable",
101
+ syllable: "Produce a single rhythmic unit centered around a vowel",
102
+ coarticulation: "Overlap articulations between adjacent sounds"
103
+ };
104
+
105
+ module.exports = dictionary;
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env node
2
+ // fill-orthography.cjs
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ const ipaDir = path.join(process.cwd(), 'ipa');
8
+ const configPath = path.join(ipaDir, 'ipa.config.js');
9
+ const alphabetPath = path.join(ipaDir, 'alphabet.js');
10
+
11
+ // -----------------------------
12
+ // CHECK FILES EXIST
13
+ // -----------------------------
14
+ if (!fs.existsSync(configPath)) {
15
+ console.error('❌ ipa.config.js not found. Run npx ipa-init first.');
16
+ process.exit(1);
17
+ }
18
+
19
+ if (!fs.existsSync(alphabetPath)) {
20
+ console.error('❌ alphabet.js not found. Run npx ipa-init first.');
21
+ process.exit(1);
22
+ }
23
+
24
+ // -----------------------------
25
+ // LOAD ALPHABET
26
+ // -----------------------------
27
+ const alphabet = require(alphabetPath);
28
+
29
+ if (!Array.isArray(alphabet) || alphabet.length === 0) {
30
+ console.error('❌ alphabet array cannot be empty');
31
+ process.exit(1);
32
+ }
33
+
34
+ // -----------------------------
35
+ // GENERATE orthography OBJECT STRING
36
+ // -----------------------------
37
+ const orthographyLines = alphabet.map(letter => ` // "${letter}": ["", []],`);
38
+ const orthographyString = `const orthography = {\n${orthographyLines.join('\n')}\n};`;
39
+
40
+ // -----------------------------
41
+ // READ EXISTING CONFIG
42
+ // -----------------------------
43
+ let fileText = fs.readFileSync(configPath, 'utf-8');
44
+
45
+ // -----------------------------
46
+ // REPLACE ONLY orthography OBJECT
47
+ // -----------------------------
48
+ fileText = fileText.replace(
49
+ /const orthography\s*=\s*\{[\s\S]*?\};/m,
50
+ orthographyString
51
+ );
52
+
53
+ // -----------------------------
54
+ // WRITE BACK UPDATED CONFIG
55
+ // -----------------------------
56
+ fs.writeFileSync(configPath, fileText, 'utf-8');
57
+ console.log(`✅ ipa.config.js updated with ${alphabet.length} letters from alphabet.js`);
@@ -38,9 +38,9 @@ function generateIPASection(core) {
38
38
  const grouped = groupByType(core);
39
39
  const sections = Object.entries(grouped).map(([type, symbols]) => {
40
40
  symbols.sort((a, b) => a.localeCompare(b));
41
- return [`// ${type.toUpperCase()}`, wrapSymbols(symbols), ''].join('\n');
41
+ return [`// ${type.toUpperCase()}`, wrapSymbols(symbols)].join('\n');
42
42
  });
43
- return ['/**', ' * IPA SYMBOL REFERENCE', ' * Copy/paste symbols as needed', '', ...sections].join('\n');
43
+ return ['/**', ' * IPA SYMBOL REFERENCE', ' * Copy/paste symbols as needed', ' */', ...sections].join('\n');
44
44
  }
45
45
 
46
46
  // -----------------------------
@@ -73,7 +73,26 @@ function generateModifierSection(modifiers) {
73
73
  const appliesTo = Array.isArray(data.appliesTo) ? data.appliesTo.join(', ') : data.appliesTo || '';
74
74
  return `${key}(${appliesTo})`;
75
75
  });
76
- return ['/**', ' * MODIFIER REFERENCE', ' * modifier(appliesTo)', ' */', '', wrapModifiers(formatted), ''].join('\n');
76
+ return ['/**', ' * MODIFIER REFERENCE', ' * modifier(appliesTo)', ' */', wrapModifiers(formatted)].join('\n');
77
+ }
78
+
79
+ // -----------------------------
80
+ // SEQUENCE MODIFIER SECTION
81
+ // -----------------------------
82
+ function generateSequenceModifierSection(modifiers) {
83
+ const sequenceModifiers = Object.entries(modifiers)
84
+ .filter(([key, data]) => {
85
+ const appliesTo = data.appliesTo;
86
+ return Array.isArray(appliesTo) ? appliesTo.includes("sequence") : appliesTo === "sequence";
87
+ })
88
+ .map(([key]) => key);
89
+
90
+ if (sequenceModifiers.length === 0) {
91
+ return '';
92
+ }
93
+
94
+ const lines = sequenceModifiers.map(m => `// ${m}`);
95
+ return ['/**', ' * SEQUENCE MODIFIERS', ' * Add as last element in array', ' */', ...lines].join('\n');
77
96
  }
78
97
 
79
98
  // -----------------------------
@@ -85,7 +104,8 @@ function generateOrthographyExport() {
85
104
  '// a: ["ɑ", ["more_rounded"]],',
86
105
  '// x̱: [',
87
106
  '// ["k", ["ejective", "aspirated"]],',
88
- '// ["x", []]',
107
+ '// ["x", []],',
108
+ '// "affricate"',
89
109
  '// ],',
90
110
  '// l: ["ɬ"]'
91
111
  ].join('\n');
@@ -98,14 +118,13 @@ function generateOrthographyExport() {
98
118
  ' * Fill in your orthography below; examples are commented out.',
99
119
  ' */',
100
120
  '',
101
- "const { parseConfig } = require('./parser.cjs');",
121
+ "const { parseConfig } = require('./parser.js');",
102
122
  '',
103
123
  'const orthography = {',
104
124
  commentedOrthography,
105
125
  '};',
106
126
  '',
107
- 'module.exports = parseConfig(orthography);',
108
- ''
127
+ 'module.exports = parseConfig(orthography);'
109
128
  ].join('\n');
110
129
  }
111
130
 
@@ -113,18 +132,34 @@ function generateOrthographyExport() {
113
132
  // MAIN GENERATOR
114
133
  // -----------------------------
115
134
  function generateConfig() {
116
- const content = [
135
+ const sections = [
117
136
  generateIPASection(core),
118
- '',
119
137
  generateModifierSection(modifiers),
120
- '',
138
+ generateSequenceModifierSection(modifiers),
121
139
  generateOrthographyExport()
122
- ].join('\n');
140
+ ].filter(s => s.length > 0);
123
141
 
124
- const outputPath = path.join(process.cwd(), 'ipa.config.js');
142
+ const content = sections.join('\n\n');
143
+
144
+ // Ensure 'ipa' folder exists
145
+ const ipaDir = path.join(process.cwd(), 'ipa');
146
+ if (!fs.existsSync(ipaDir)) {
147
+ fs.mkdirSync(ipaDir, { recursive: true });
148
+ }
149
+
150
+ // Write file inside 'ipa' folder
151
+ const outputPath = path.join(ipaDir, 'ipa.config.js');
125
152
  fs.writeFileSync(outputPath, content, 'utf-8');
126
153
 
127
- console.log('✅ ipa.config.js generated with parseConfig() export');
154
+ console.log('✅ ipa/ipa.config.js generated with parseConfig() export');
155
+
156
+ // Write alphabet.js if it doesn't exist
157
+ const alphabetPath = path.join(ipaDir, 'alphabet.js');
158
+ if (!fs.existsSync(alphabetPath)) {
159
+ const alphabetContent = 'const alphabet = [];\nmodule.exports = alphabet;\n';
160
+ fs.writeFileSync(alphabetPath, alphabetContent, 'utf-8');
161
+ console.log('✅ ipa/alphabet.js generated with empty alphabet array');
162
+ }
128
163
  }
129
164
 
130
165
  // RUN
@@ -86,11 +86,48 @@ function generateConlang({ alphabet, mod1Chance, mod2Chance, affricateChance })
86
86
  orthography[letter] = [
87
87
  [ipaKey, modifiersList],
88
88
  [fricKey, []],
89
+ "affricate"
89
90
  ];
90
91
  return;
91
92
  }
92
93
  }
93
94
 
95
+ // Diphthong (vowel + vowel sequence)
96
+ if (baseObj?.type === 'vowel' && Math.random() < 0.3) {
97
+ const vowels = Object.keys(core).filter(k => core[k].type === 'vowel');
98
+ const secondVowel = pickRandom(vowels);
99
+ orthography[letter] = [
100
+ [ipaKey, modifiersList],
101
+ [secondVowel, []],
102
+ "diphthong"
103
+ ];
104
+ return;
105
+ }
106
+
107
+ // Syllable (consonant + vowel)
108
+ if (baseObj?.type === 'consonant' && Math.random() < 0.3) {
109
+ const vowels = Object.keys(core).filter(k => core[k].type === 'vowel');
110
+ const vowel = pickRandom(vowels);
111
+ orthography[letter] = [
112
+ [ipaKey, modifiersList],
113
+ [vowel, []],
114
+ "syllable"
115
+ ];
116
+ return;
117
+ }
118
+
119
+ // Coarticulation (consonant + consonant)
120
+ if (baseObj?.type === 'consonant' && Math.random() < 0.15) {
121
+ const consonants = Object.keys(core).filter(k => core[k].type === 'consonant');
122
+ const secondConsonant = pickRandom(consonants);
123
+ orthography[letter] = [
124
+ [ipaKey, modifiersList],
125
+ [secondConsonant, []],
126
+ "coarticulation"
127
+ ];
128
+ return;
129
+ }
130
+
94
131
  orthography[letter] = [ipaKey, modifiersList];
95
132
  });
96
133
 
@@ -111,7 +148,15 @@ async function main() {
111
148
  affricateChance: aff,
112
149
  });
113
150
 
114
- const outPath = path.join(process.cwd(), 'generatedConlang.js');
151
+ // Ensure 'ipa' folder exists
152
+ const ipaDir = path.join(process.cwd(), 'ipa');
153
+ if (!fs.existsSync(ipaDir)) {
154
+ fs.mkdirSync(ipaDir, { recursive: true });
155
+ }
156
+
157
+ // Write file inside 'ipa' folder
158
+ const outPath = path.join(ipaDir, 'generatedConlang.js');
159
+
115
160
  const lines = Object.entries(orthography).map(
116
161
  ([l, v]) => ` ${JSON.stringify(l)}:${JSON.stringify(v)}`
117
162
  );
@@ -119,7 +164,7 @@ async function main() {
119
164
  const content = `/** Generated conlang orthography */\nconst generatedOrthography = {\n${lines.join(',\n')}\n};\nmodule.exports = generatedOrthography;\n`;
120
165
 
121
166
  fs.writeFileSync(outPath, content, 'utf-8');
122
- console.log('✅ generatedConlang.js written');
167
+ console.log('✅ ipa/generatedConlang.js written');
123
168
  }
124
169
 
125
170
  main();
@@ -0,0 +1,41 @@
1
+ const dictionary = require('./dictionary');
2
+
3
+ function getDefinitions(phoneme) {
4
+ const steps = [];
5
+
6
+ function collect(p) {
7
+ if (p.features?.place && dictionary[p.features.place]) {
8
+ steps.push(dictionary[p.features.place]);
9
+ }
10
+
11
+ if (p.features?.manner && dictionary[p.features.manner]) {
12
+ steps.push(dictionary[p.features.manner]);
13
+ }
14
+
15
+ if (p.features?.voicing && dictionary[p.features.voicing]) {
16
+ steps.push(dictionary[p.features.voicing]);
17
+ }
18
+
19
+ if (p.airstream && dictionary[p.airstream]) {
20
+ steps.push(dictionary[p.airstream]);
21
+ }
22
+
23
+ if (p.modifiersApplied) {
24
+ for (const mod of p.modifiersApplied) {
25
+ if (dictionary[mod]) steps.push(dictionary[mod]);
26
+ }
27
+ }
28
+
29
+ if (p.components) {
30
+ for (const comp of p.components) {
31
+ collect(comp); // IMPORTANT: no string recursion
32
+ }
33
+ }
34
+ }
35
+
36
+ collect(phoneme);
37
+
38
+ return steps.join(". ") + ".";
39
+ }
40
+
41
+ module.exports = { getDefinitions, dictionary };
package/index.js CHANGED
@@ -3,6 +3,8 @@ const core = require("./core.js"); // base consonants & vowels
3
3
  const modifiers = require("./modifiers.js"); // diacritics, tones, suprasegmentals
4
4
  const rules = require("./rules.js"); // modifier compatibility rules
5
5
  const parser = require("./parser.js"); // parser (parsePhoneme, parseUnit, parseConfig)
6
+ const dictionary = require("./dictionary.js"); //instructional definitions
7
+ const getDefinitions = require("./get-definitions.js"); //compiles and formats instructions
6
8
 
7
9
  // Export the layers so users can access them if needed
8
10
  const { parsePhoneme, parseUnit, parseConfig } = parser;
@@ -14,5 +16,7 @@ module.exports = {
14
16
  parser,
15
17
  parsePhoneme,
16
18
  parseUnit,
17
- parseConfig
19
+ parseConfig,
20
+ dictionary,
21
+ getDefinitions
18
22
  };
package/modifiers.js CHANGED
@@ -20,34 +20,47 @@ const modifiers = {
20
20
  more_rounded: {
21
21
  symbol: "̹",
22
22
  appliesTo: "vowel",
23
- effects: { rounding: "more" }
23
+ effects: { roundingModifier: "more" }
24
24
  },
25
25
  less_rounded: {
26
26
  symbol: "̜",
27
27
  appliesTo: "vowel",
28
- effects: { rounding: "less" }
28
+ effects: { roundingModifier: "less" }
29
29
  },
30
30
 
31
+ // PLACE: PRIMARY OVERRIDES
32
+ dental: {
33
+ symbol: "̪",
34
+ appliesTo: "consonant",
35
+ effects: { place: "dental" }
36
+ },
37
+ linguolabial: {
38
+ symbol: "̼",
39
+ appliesTo: "consonant",
40
+ effects: { place: "linguolabial" }
41
+ },
42
+
43
+ // PLACE: SHIFTS (DO NOT OVERRIDE)
31
44
  advanced: {
32
45
  symbol: "̟",
33
46
  appliesTo: "consonant",
34
- effects: { placeModifier: "advanced" }
47
+ effects: { placeShift: "advanced" }
35
48
  },
36
49
  retracted: {
37
50
  symbol: "̠",
38
51
  appliesTo: "consonant",
39
- effects: { placeModifier: "retracted" }
52
+ effects: { placeShift: "retracted" }
40
53
  },
41
54
 
42
55
  centralized: {
43
56
  symbol: "̈",
44
57
  appliesTo: "vowel",
45
- effects: { backness: "centralized" }
58
+ effects: { centralization: "centralized" }
46
59
  },
47
60
  mid_centralized: {
48
61
  symbol: "̽",
49
62
  appliesTo: "vowel",
50
- effects: { backness: "mid-centralized" }
63
+ effects: { centralization: "mid" }
51
64
  },
52
65
 
53
66
  syllabic: {
@@ -61,29 +74,20 @@ const modifiers = {
61
74
  effects: { syllabic: false }
62
75
  },
63
76
 
64
- dental: {
65
- symbol: "̪",
66
- appliesTo: "consonant",
67
- effects: { placeModifier: "dental" }
68
- },
69
77
  apical: {
70
78
  symbol: "̺",
71
79
  appliesTo: "consonant",
72
- effects: { articulation: "apical" }
80
+ effects: { tonguePart: "apical" }
73
81
  },
74
82
  laminal: {
75
83
  symbol: "̻",
76
84
  appliesTo: "consonant",
77
- effects: { articulation: "laminal" }
85
+ effects: { tonguePart: "laminal" }
78
86
  },
79
87
 
80
- velarized_or_pharyngealized: {
81
- symbol: "ˠ",
82
- appliesTo: "consonant",
83
- effects: { secondaryArticulation: "velarized_or_pharyngealized" }
84
- },
88
+ // SECONDARY ARTICULATION
85
89
  velarized: {
86
- symbol: "̴",
90
+ symbol: "ˠ",
87
91
  appliesTo: "consonant",
88
92
  effects: { secondaryArticulation: "velarized" }
89
93
  },
@@ -115,6 +119,7 @@ const modifiers = {
115
119
  effects: { rhotic: true }
116
120
  },
117
121
 
122
+ // LENGTH
118
123
  length_half_long: {
119
124
  symbol: "ˑ",
120
125
  appliesTo: ["vowel", "consonant"],
@@ -131,15 +136,7 @@ const modifiers = {
131
136
  effects: { length: "extra-short" }
132
137
  },
133
138
 
134
- voiceless_flap: {
135
- symbol: "̥̆",
136
- appliesTo: "consonant",
137
- effects: {
138
- voicing: "voiceless",
139
- mannerModifier: "flap"
140
- }
141
- },
142
-
139
+ // PHONATION
143
140
  creaky_voice: {
144
141
  symbol: "̰",
145
142
  appliesTo: ["vowel", "consonant"],
@@ -151,12 +148,7 @@ const modifiers = {
151
148
  effects: { phonation: "breathy" }
152
149
  },
153
150
 
154
- linguolabial: {
155
- symbol: "̼",
156
- appliesTo: "consonant",
157
- effects: { placeModifier: "linguolabial" }
158
- },
159
-
151
+ // HEIGHT MODIFICATION
160
152
  raised: {
161
153
  symbol: "̝",
162
154
  appliesTo: ["vowel", "consonant"],
@@ -168,6 +160,7 @@ const modifiers = {
168
160
  effects: { heightModifier: "lowered" }
169
161
  },
170
162
 
163
+ // TONGUE ROOT
171
164
  advanced_tongue_root: {
172
165
  symbol: "̘",
173
166
  appliesTo: "vowel",
@@ -179,8 +172,7 @@ const modifiers = {
179
172
  effects: { tongueRoot: "retracted" }
180
173
  },
181
174
 
182
- // TONES / ACCENTS
183
-
175
+ // TONE (unified — removed pitch duplication)
184
176
  high_tone: {
185
177
  symbol: "́",
186
178
  appliesTo: ["vowel", "consonant"],
@@ -212,17 +204,6 @@ const modifiers = {
212
204
  effects: { tone: "extra-low" }
213
205
  },
214
206
 
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
207
  downstep: {
227
208
  symbol: "ꜜ",
228
209
  appliesTo: ["vowel", "consonant"],
@@ -239,12 +220,18 @@ const modifiers = {
239
220
  appliesTo: "consonant",
240
221
  effects: { release: "none" }
241
222
  },
242
- //ejective
223
+
243
224
  ejective: {
244
- symbol: "ʼ",
245
- appliesTo: "consonant",
246
- effects: { airstream: "egressive_glottalic" } // or just mark it as ejective
247
- }
225
+ symbol: "ʼ",
226
+ appliesTo: "consonant",
227
+ effects: { airstream: "egressive_glottalic" }
228
+ },
229
+
230
+ // SEQUENCE MODIFIERS
231
+ diphthong: { appliesTo: "sequence", effects: { type: "diphthong" } },
232
+ affricate: { appliesTo: "sequence", effects: { type: "affricate" } },
233
+ syllable: { appliesTo: "sequence", effects: { type: "syllable" } },
234
+ coarticulation: { appliesTo: "sequence", effects: { type: "coarticulation" } }
248
235
  };
249
236
 
250
237
  module.exports = modifiers;
package/package.json CHANGED
@@ -1,19 +1,26 @@
1
1
  {
2
2
  "name": "ipa-core",
3
- "version": "1.1.13",
3
+ "version": "1.3.1",
4
4
  "description": "Language-agnostic IPA core with features",
5
+ "repository": "github:Cody253/ipa-core",
6
+ "homepage": "https://github.com/Cody253/ipa-core#readme",
7
+ "bugs": {
8
+ "url": "https://github.com/Cody253/ipa-core/issues"
9
+ },
5
10
  "license": "ISC",
6
11
  "author": "Cody Bruno",
7
12
  "type": "commonjs",
8
13
  "bin": {
9
14
  "make-conlang": "generate-conlang.js",
10
- "ipa-init": "generate-config.js"
15
+ "ipa-init": "generate-config.js",
16
+ "fill-orthography": "fill-orthography.js"
11
17
  },
12
18
  "main": "index.js",
13
19
  "scripts": {
14
20
  "test": "node test.js",
15
21
  "init:ipa": "node generate-config.js",
16
- "make-conlang": "node generate-conlang.js"
22
+ "make-conlang": "node generate-conlang.js",
23
+ "fill-orthography": "node fill-orthography.js"
17
24
  },
18
25
  "keywords": [
19
26
  "ipa",
package/parser.js CHANGED
@@ -16,8 +16,14 @@ function parsePhoneme(base, appliedModifiers = []) {
16
16
  throw new Error(`Unknown base phoneme: ${base}`);
17
17
  }
18
18
 
19
- let features = { ...baseObj.features };
20
- const modifiersApplied = [];
19
+ // Start with type and airstream from core
20
+ let phoneme = {
21
+ base,
22
+ type: baseObj.type, // consonant, vowel, etc.
23
+ airstream: baseObj.airstream, // pulmonic, egressive_glottalic, etc.
24
+ features: { ...baseObj.features },
25
+ modifiersApplied: []
26
+ };
21
27
 
22
28
  for (const modKey of appliedModifiers) {
23
29
  const mod = modifiers[modKey];
@@ -26,29 +32,25 @@ function parsePhoneme(base, appliedModifiers = []) {
26
32
  }
27
33
 
28
34
  const incompatible = rules[modKey]?.incompatibleWith || [];
29
- const conflict = modifiersApplied.find(applied => incompatible.includes(applied));
35
+ const conflict = phoneme.modifiersApplied.find(applied => incompatible.includes(applied));
30
36
  if (conflict) {
31
37
  console.warn(`Modifier "${modKey}" is incompatible with already applied "${conflict}". Skipping.`);
32
38
  continue;
33
39
  }
34
40
 
35
- if (mod.features) {
36
- features = { ...features, ...mod.features };
41
+ if (mod.effects) {
42
+ phoneme.features = { ...phoneme.features, ...mod.effects };
37
43
  } else {
38
- features[modKey] = true;
44
+ phoneme.features[modKey] = true;
39
45
  }
40
46
 
41
- modifiersApplied.push(modKey);
47
+ phoneme.modifiersApplied.push(modKey);
42
48
  }
43
49
 
44
- return {
45
- base,
46
- symbol: baseObj.symbol || base,
47
- features,
48
- modifiersApplied
49
- };
50
+ return phoneme;
50
51
  }
51
52
 
53
+
52
54
  /**
53
55
  * Helper to create a phoneme unit for orthography config.
54
56
  * Can be a single phoneme or a sequence object.
@@ -56,10 +58,11 @@ function parsePhoneme(base, appliedModifiers = []) {
56
58
  * @param {string[]} appliedModifiers - Modifiers for single phoneme (ignored if sequence)
57
59
  * @returns {object}
58
60
  */
59
- function parseUnit(unit, appliedModifiers = []) {
61
+ function parseUnit(unit, appliedModifiers = null) {
60
62
  // if unit is a string, treat as single phoneme
61
63
  if (typeof unit === "string") {
62
- return { type: "single", base: unit, modifiers: appliedModifiers };
64
+ const mods = Array.isArray(appliedModifiers) ? appliedModifiers : [];
65
+ return { type: "single", base: unit, modifiers: mods };
63
66
  }
64
67
 
65
68
  // if unit is an object with multiple phonemes, treat as sequence
@@ -69,12 +72,79 @@ function parseUnit(unit, appliedModifiers = []) {
69
72
  const mods = unit[base] || [];
70
73
  sequence.push({ base, modifiers: mods });
71
74
  }
72
- return { type: "sequence", sequence };
75
+ const type = (appliedModifiers && typeof appliedModifiers === "string") ? appliedModifiers : "sequence";
76
+ return { type, sequence };
73
77
  }
74
78
 
75
79
  throw new Error(`Invalid unit: ${unit}`);
76
80
  }
77
81
 
82
+ /**
83
+ * Validates a sequence against its rules.
84
+ * @param {object} sequenceObj - { type, components }
85
+ * @returns {object} - validated sequence or throws error
86
+ */
87
+ function validateSequence(sequenceObj) {
88
+ const { type, components } = sequenceObj;
89
+ const rule = rules[type];
90
+
91
+ if (!rule) {
92
+ return sequenceObj;
93
+ }
94
+
95
+ if (rule.length && components.length !== rule.length) {
96
+ throw new Error(`${type} requires exactly ${rule.length} components, got ${components.length}`);
97
+ }
98
+
99
+ if (rule.minLength && components.length < rule.minLength) {
100
+ throw new Error(`${type} requires at least ${rule.minLength} components, got ${components.length}`);
101
+ }
102
+
103
+ if (rule.requires) {
104
+ for (const req of rule.requires) {
105
+ const component = components[req.position];
106
+ if (!component) {
107
+ throw new Error(`${type} requires component at position ${req.position}`);
108
+ }
109
+ if (req.type && component.type !== req.type) {
110
+ throw new Error(`${type} requires position ${req.position} to be type "${req.type}", got "${component.type}"`);
111
+ }
112
+ if (req.features) {
113
+ for (const [key, value] of Object.entries(req.features)) {
114
+ if (component.features[key] !== value) {
115
+ throw new Error(`${type} requires position ${req.position} to have features.${key}="${value}", got "${component.features[key]}"`);
116
+ }
117
+ }
118
+ }
119
+ }
120
+ }
121
+
122
+ if (rule.requiresAll) {
123
+ for (const req of rule.requiresAll) {
124
+ const invalid = components.find(c => req.type && c.type !== req.type);
125
+ if (invalid) {
126
+ throw new Error(`${type} requires all components to be type "${req.type}", but found "${invalid.type}"`);
127
+ }
128
+ }
129
+ }
130
+
131
+ if (rule.requiresFromPosition) {
132
+ for (const [pos, reqs] of Object.entries(rule.requiresFromPosition)) {
133
+ const component = components[parseInt(pos)];
134
+ if (!component) {
135
+ throw new Error(`${type} requires component at position ${pos}`);
136
+ }
137
+ for (const req of reqs) {
138
+ if (req.type && component.type !== req.type) {
139
+ throw new Error(`${type} requires position ${pos} to be type "${req.type}", got "${component.type}"`);
140
+ }
141
+ }
142
+ }
143
+ }
144
+
145
+ return sequenceObj;
146
+ }
147
+
78
148
  /**
79
149
  * Parses a unit (single or sequence) into full feature objects
80
150
  * @param {object} unitObj
@@ -85,8 +155,14 @@ function parseUnitObj(unitObj) {
85
155
  return parsePhoneme(unitObj.base, unitObj.modifiers);
86
156
  }
87
157
 
88
- if (unitObj.type === "sequence") {
89
- return unitObj.sequence.map(u => parsePhoneme(u.base, u.modifiers));
158
+ if (unitObj.type === "sequence" || unitObj.type === "affricate" || unitObj.type === "diphthong" || unitObj.type === "syllable" || unitObj.type === "coarticulation") {
159
+ const components = unitObj.sequence.map((u, index) => {
160
+ const parsed = parsePhoneme(u.base, u.modifiers);
161
+ parsed.position = index;
162
+ return parsed;
163
+ });
164
+ const sequenceObj = { type: unitObj.type, components };
165
+ return validateSequence(sequenceObj);
90
166
  }
91
167
 
92
168
  throw new Error(`Unknown unit type: ${unitObj.type}`);
@@ -110,7 +186,7 @@ function parseConfig(config) {
110
186
  let unitObj;
111
187
 
112
188
  // Already a parseUnit object
113
- if (val && typeof val === "object" && val.type && (val.type === "single" || val.type === "sequence")) {
189
+ if (val && typeof val === "object" && val.type && (val.type === "single" || val.sequence)) {
114
190
  unitObj = val;
115
191
  }
116
192
  // Shorthand single phoneme: ["base", ["modifiers"]]
@@ -119,10 +195,13 @@ function parseConfig(config) {
119
195
  const mods = Array.isArray(val[1]) ? val[1] : [];
120
196
  unitObj = { type: "single", base, modifiers: mods };
121
197
  }
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 };
198
+ // Shorthand sequence: [["base1", ["mods1"]], ["base2", ["mods2"]], ..., "affricate"]
199
+ else if (Array.isArray(val) && val.every(el => Array.isArray(el) || typeof el === "string")) {
200
+ const stringElements = val.filter(el => typeof el === "string");
201
+ const sequenceModifier = stringElements[0] || null;
202
+ const arrayElements = val.filter(el => Array.isArray(el));
203
+ const sequence = arrayElements.map(([base, mods]) => ({ base, modifiers: Array.isArray(mods) ? mods : [] }));
204
+ unitObj = { type: sequenceModifier || "sequence", sequence };
126
205
  }
127
206
  else {
128
207
  throw new Error(`Invalid orthography entry for "${letter}": ${JSON.stringify(val)}`);
package/rules.js CHANGED
@@ -1,38 +1,45 @@
1
- //modifier compatability
1
+ // modifier compatibility
2
2
  const rules = {
3
- // Voicing / phonation
3
+ // VOICING / PHONATION
4
4
  voiceless: { incompatibleWith: ["voiced", "creaky_voice", "breathy_voice"] },
5
- voiced: { incompatibleWith: ["voiceless", "voiceless_flap"] },
6
- voiceless_flap: { incompatibleWith: ["voiced", "aspirated"] },
5
+ voiced: { incompatibleWith: ["voiceless"] },
7
6
  creaky_voice: { incompatibleWith: ["voiceless"] },
8
7
  breathy_voice: { incompatibleWith: ["voiceless"] },
9
8
 
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: [] },
9
+ // PLACE: SHIFTS (mutually exclusive)
10
+ advanced: { incompatibleWith: ["retracted"] },
11
+ retracted: { incompatibleWith: ["advanced"] },
12
+
13
+ // PLACE: PRIMARY OVERRIDES
14
+ // (not inherently incompatible — last one wins in processing)
17
15
  dental: { incompatibleWith: [] },
18
- apical: { incompatibleWith: [] },
19
- laminal: { incompatibleWith: [] },
20
- velarized: { incompatibleWith: ["palatalized", "advanced", "retracted"] },
16
+ linguolabial: { incompatibleWith: [] },
17
+
18
+ // TONGUE PART (mutually exclusive)
19
+ apical: { incompatibleWith: ["laminal"] },
20
+ laminal: { incompatibleWith: ["apical"] },
21
21
 
22
- // Syllabicity
22
+ // SECONDARY ARTICULATION
23
+ // These can stack in real phonetics, but we constrain for clarity
24
+ palatalized: { incompatibleWith: ["velarized", "pharyngealized"] },
25
+ velarized: { incompatibleWith: ["palatalized"] },
26
+ pharyngealized: { incompatibleWith: ["palatalized"] },
27
+ labialized: { incompatibleWith: [] },
28
+
29
+ // SYLLABICITY
23
30
  syllabic: { incompatibleWith: ["non_syllabic"] },
24
31
  non_syllabic: { incompatibleWith: ["syllabic"] },
25
32
 
26
- // Nasalization / rhoticity
33
+ // NASALIZATION / RHOTICITY
27
34
  nasalized: { incompatibleWith: [] },
28
35
  rhoticity: { incompatibleWith: [] },
29
36
 
30
- // Length / duration
37
+ // LENGTH / DURATION
31
38
  extra_short: { incompatibleWith: ["length_half_long", "length_long"] },
32
39
  length_half_long: { incompatibleWith: ["extra_short", "length_long"] },
33
40
  length_long: { incompatibleWith: ["extra_short", "length_half_long"] },
34
41
 
35
- // Segment-level tones / accents
42
+ // TONE
36
43
  high_tone: { incompatibleWith: ["low_tone", "falling_tone", "extra_low_tone"] },
37
44
  low_tone: { incompatibleWith: ["high_tone", "rising_tone", "extra_high_tone"] },
38
45
  rising_tone: { incompatibleWith: ["falling_tone", "low_tone"] },
@@ -41,12 +48,63 @@ const rules = {
41
48
  extra_low_tone: { incompatibleWith: ["extra_high_tone", "high_tone"] },
42
49
  downstep: { incompatibleWith: [] },
43
50
  upstep: { incompatibleWith: [] },
44
- extra_high_pitch: { incompatibleWith: ["extra_low_pitch"] },
45
- extra_low_pitch: { incompatibleWith: ["extra_high_pitch"] },
46
51
 
47
- // Special consonant markers
48
- aspirated: { incompatibleWith: ["voiceless_flap", "no_audible_release"] },
49
- no_audible_release: { incompatibleWith: ["aspirated"] }
52
+ // RELEASE / ASPIRATION
53
+ aspirated: { incompatibleWith: ["no_audible_release"] },
54
+ no_audible_release: { incompatibleWith: ["aspirated"] },
55
+
56
+ // VOWEL QUALITY (non-conflicting dimensions)
57
+ more_rounded: { incompatibleWith: [] },
58
+ less_rounded: { incompatibleWith: [] },
59
+
60
+ centralized: { incompatibleWith: ["mid_centralized"] },
61
+ mid_centralized: { incompatibleWith: ["centralized"] },
62
+
63
+ raised: { incompatibleWith: ["lowered"] },
64
+ lowered: { incompatibleWith: ["raised"] },
65
+
66
+ // TONGUE ROOT (single axis, but isolated from others)
67
+ advanced_tongue_root: { incompatibleWith: ["retracted_tongue_root"] },
68
+ retracted_tongue_root: { incompatibleWith: ["advanced_tongue_root"] },
69
+
70
+ // AIRSTREAM MECHANISM (independent feature class)
71
+ ejective: { incompatibleWith: [] },
72
+
73
+ // SEQUENCE-LEVEL RULES
74
+
75
+ affricate: {
76
+ requires: [
77
+ { position: 0, features: { manner: "plosive" } },
78
+ { position: 1, features: { manner: "fricative" } }
79
+ ],
80
+ length: 2
81
+ },
82
+
83
+ diphthong: {
84
+ requiresAll: [
85
+ { type: "vowel" }
86
+ ],
87
+ minLength: 2
88
+ },
89
+
90
+ coarticulation: {
91
+ requiresAll: [
92
+ { type: "consonant" }
93
+ ],
94
+ minLength: 2
95
+ },
96
+
97
+ syllable: {
98
+ requires: [
99
+ { position: 0, type: "consonant" }
100
+ ],
101
+ requiresFromPosition: {
102
+ 1: [
103
+ { type: "vowel" }
104
+ ]
105
+ },
106
+ minLength: 2
107
+ }
50
108
  };
51
109
 
52
- module.exports = rules
110
+ module.exports = rules;
package/test.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // test.js
2
2
 
3
3
  const { parseUnit, parseConfig } = require("./parser");
4
+ const { getDefinitions } = require("./get-definitions");
4
5
 
5
6
  // Sample orthography config
6
7
  const orthography = {
@@ -14,25 +15,57 @@ const orthography = {
14
15
  x̱: parseUnit({
15
16
  k: ["ejective", "aspirated"],
16
17
  x: []
17
- }),
18
+ }, "affricate"),
18
19
 
19
20
  // Another single consonant
20
- l: parseUnit("ɬ")
21
+ l: parseUnit("ɬ"),
22
+
23
+ // Consonant with place modifiers
24
+ t̪: parseUnit("t", ["dental"]),
25
+ s̻: parseUnit("s", ["laminal"]),
26
+ k̠: parseUnit("k", ["retracted"])
21
27
  };
22
28
 
23
29
  // Another sample orthography config using shorthand
24
30
  const shorthandOrthography = {
25
- // A single consonant with a modifier
26
- k: ["k", ["aspirated"]],
31
+ // A single consonant
32
+ k: ["k"],
27
33
 
28
34
  // A single vowel with a modifier
29
35
  a: ["ɑ", ["more_rounded"]],
30
36
 
31
- // A consonant sequence (affricate)
32
- x̱: [[
33
- 'k', ["ejective", "aspirated"],
34
- 'x', []
35
- ]],
37
+ // A consonant sequence (affricate) with new string shorthand
38
+ x̱: [
39
+ ["k", ["ejective", "aspirated"]],
40
+ ["x", []],
41
+ "affricate"
42
+ ],
43
+
44
+ // Diphthong example
45
+ ai: [
46
+ ["ɑ"],
47
+ ["i"],
48
+ "diphthong"
49
+ ],
50
+
51
+ // Syllable example (e.g., CV - consonant followed by vowel)
52
+ na: [
53
+ ["n"],
54
+ ["ɑ"],
55
+ "syllable"
56
+ ],
57
+
58
+ // Coarticulation example (e.g., labial-velar)
59
+ w: [
60
+ ["ʍ"],
61
+ "coarticulation"
62
+ ],
63
+
64
+ // Plain sequence (no sequence modifier)
65
+ kl: [
66
+ ["k"],
67
+ ["l"]
68
+ ],
36
69
 
37
70
  // Another single consonant
38
71
  l: ["ɬ"]
@@ -48,13 +81,26 @@ console.log(JSON.stringify(parsed, null, 2));
48
81
  // Optional: inspect a single phoneme
49
82
  const singlePhoneme = parsed.k;
50
83
  console.log("\n=== Single Phoneme 'k' Features ===");
51
- console.log(singlePhoneme);
84
+ console.log(JSON.stringify(singlePhoneme, null, 2));
52
85
 
53
86
  // Optional: inspect a sequence
54
87
  const affricateSequence = parsed.x̱;
55
88
  console.log("\n=== Affricate Sequence 'x̱' Features ===");
56
- console.log(affricateSequence);
89
+ console.log(JSON.stringify(affricateSequence, null, 2));
90
+
91
+ // const parsedShorthand = parseConfig(shorthandOrthography);
92
+ // console.log("\n=== Parsed Output2 ===");
93
+ // console.log(JSON.stringify(parsedShorthand, null, 2));
94
+
95
+ // Definitions
96
+
97
+
98
+ console.log("\n=== Definitions for 'k' ===");
99
+ console.log(getDefinitions(parsed.k));
100
+
101
+ console.log("\n=== Definitions for 'x̱' (affricate) ===");
102
+ console.log(getDefinitions(parsed.x̱));
103
+
104
+ console.log("\n=== Definitions for 'a' (vowel) ===");
105
+ console.log(getDefinitions(parsed.a));
57
106
 
58
- const parsedShorthand = parseConfig(shorthandOrthography);
59
- console.log("\n=== Parsed Output2 ===");
60
- console.log(JSON.stringify(parsedShorthand, null, 2));