Compare commits

...

97 commits

Author SHA1 Message Date
Sorrel
96bbb274df
Create LICENSE 2021-04-14 20:34:07 -04:00
Sorrel Bri
bb69a9ffa4 patch test bug incorrectly oriented square brackets 2020-05-21 19:36:43 -07:00
Sorrel Bri
dfae87e408 stub working parse of or Operation on sets; phoneList being read as setAlias 2020-05-21 19:02:18 -07:00
Sorrel Bri
c264b56c2e stub AST results for set definition with join 2020-05-18 22:37:19 -07:00
Sorrel Bri
73761e6f60 fix syntax errors in example latl file 2020-05-18 22:02:00 -07:00
Sorrel Bri
bb8c05a579 add support for set aliases 2020-05-09 22:18:07 -07:00
Sorrel Bri
9619b4a07c update latl README with set definition 2020-05-09 16:24:00 -07:00
Sorrel Bri
abfe14b410 construct AST properly for multi set definitions 2020-05-09 15:22:01 -07:00
Sorrel Bri
40aec30537 parse AST for single set definition 2020-05-08 23:32:49 -07:00
Sorrel Bri
3d4d1cd66e hack set definition postprocessors 2020-05-07 23:24:19 -07:00
Sorrel Bri
dee27b0d30 init codeGenerator in latl 2020-05-06 22:31:15 -07:00
Sorrel Bri
432630e600 add postprocessors to grammar.ne for cleaning tree of empty nodes 2020-04-14 22:03:56 -07:00
Sorrel Bri
aa19d42a11 stub parser 2020-03-27 15:55:45 -07:00
Sorrel Bri
7c75be543f define tokens for lexing set [concat], [dissoc] operations 2020-03-15 21:40:31 -07:00
Sorrel Bri
a7dad0d3e5 define tokens for lexing set [] in, yield [] operations 2020-03-14 23:06:15 -07:00
Sorrel Bri
2634e35a01 define tokens for lexing set definitions, aliases, or operation, and aliases 2020-03-14 22:14:31 -07:00
Sorrel Bri
6e230de7f0 stub language spec and example files 2020-03-13 14:13:01 -07:00
Sorrel Bri
f5a712557c init parser with nearley 2020-03-12 12:38:52 -07:00
Sorrel Bri
e08500a047 init lexer with moo 2020-03-11 21:09:41 -07:00
Sorrel Bri
3f2c822c55 patch phoneme feature bug, add initWaffleState 2020-03-04 16:27:26 -08:00
Sorrel Bri
ad364bbd07 debug for waffle present 2020-03-04 15:15:07 -08:00
Sorrel Bri
c653653ba1 support for epoch and features, lexicon support still buggy 2020-03-03 20:32:54 -08:00
Sorrel Bri
20bede405f patch issues with phone token context 2020-03-03 18:12:38 -08:00
Sorrel Bri
5c2138e04f patch for improved character support in spacing modifier range 2020-03-03 01:33:28 -08:00
Sorrel Bri
d3ebe61577 add latl parse for features 2020-03-03 01:20:06 -08:00
Sorrel Bri
da45699c59 debug parse_latl, hook up Output to state 2020-03-02 22:46:08 -08:00
Sorrel Bri
78b513c9be add support for extended IPA, greek characters, etc, with phone token 2020-03-02 20:48:42 -08:00
Sorrel Bri
4ee5bc0f78 hook input on LatlOutput to dispatch function 2020-03-02 19:14:51 -08:00
Sorrel Bri
49419a39ee test green for run from parsed epoch latl 2020-03-02 19:06:21 -08:00
Sorrel Bri
35f815a9c9 add AST parsing support for epoch definitions 2020-03-02 15:06:59 -08:00
Sorrel Bri
64b2b5d332 stub AST parser 2020-03-01 23:17:57 -08:00
Sorrel Bri
686a1b1ffc refactor tokenize to return token objects 2020-03-01 23:03:33 -08:00
Sorrel Bri
6bd425ac34 add tokenizer for epoch, feature, and lexicon tokens 2020-03-01 22:42:35 -08:00
Sorrel Bri
d5d1eb2fa2 stub parse_latl reducer action 2020-03-01 20:55:23 -08:00
Sorrel Bri
6bdd0a9d65 patch revert some of Epoch refactor 2020-03-01 15:49:56 -08:00
Sorrel Bri
74bbca028f refactor Epoch component 2020-03-01 15:41:25 -08:00
Sorrel Bri
c13cc33697 patch to fix error messaging tests 2020-03-01 12:59:41 -08:00
Sorrel Bri
22b4adb547 add router to componenet tests 2020-02-29 12:59:33 -08:00
Sorrel Bri
201b4ca8b9 stub latl reducer 2020-02-29 12:52:20 -08:00
Sorrel Bri
ff681e6223 stub style for latl 2020-02-29 12:44:02 -08:00
Sorrel Bri
b9b30014c7 stub #/latl route components 2020-02-28 18:41:42 -08:00
Sorrel Bri
4b77f69cd3 init latl 2020-02-28 17:44:36 -08:00
Sorrel Bri
ca7aea4ce3 patch with updated name and title 2020-02-28 17:07:20 -08:00
Sorrel Bri
2d31a20f4b patch grid gap style 2020-02-28 13:30:48 -08:00
Sorrel Bri
156b05ec42 patch typos in readme 2020-02-28 13:27:55 -08:00
Sorrel Bri
0dfd40dff3 patch typos in readme 2020-02-28 13:14:29 -08:00
Sorrel Bri
e570bdfeff add feature remove button, style buttons 2020-02-28 13:12:19 -08:00
Sorrel Bri
e7a7673d68 add clear output to Options component, remove future options 2020-02-28 11:58:16 -08:00
Sorrel Bri
d87a99c498 patch epochs to update with parent 2020-02-28 11:47:01 -08:00
Sorrel Bri
a74d387834 add error messaging for improper rules 2020-02-28 11:13:32 -08:00
Sorrel Bri
d1e1d8e1c6 add support for epoch parents 2020-02-27 14:50:25 -08:00
Sorrel Bri
be10c6923f add usage documentation to readme 2020-02-27 13:52:15 -08:00
Sorrel Bri
44990abd68 style global padding and improve margin around components 2020-02-26 16:48:50 -08:00
Sorrel Bri
364a3fee29 first deploy 2020-02-26 16:36:06 -08:00
Sorrel Bri
c5b7e13029 add style to results component 2020-02-26 16:27:45 -08:00
Sorrel Bri
953c403f3b style features and options components 2020-02-26 16:16:52 -08:00
Sorrel Bri
7f868c5a26 add fonts, standardize textarea size 2020-02-26 15:23:39 -08:00
Sorrel Bri
228e31a41a add responsive grid style to app layout 2020-02-26 14:42:47 -08:00
Sorrel Bri
86c4d3698d refactor components for code cleanliness 2020-02-26 14:06:04 -08:00
Sorrel Bri
936e09d00a add main color theme 2020-02-21 21:45:53 -08:00
Sorrel Bri
3f2b4e6618 refactor components to remove unused useState hooks 2020-02-21 20:55:17 -08:00
Sorrel Bri
6f55f0ac39 refactor Output component to display multiple results 2020-02-21 20:21:06 -08:00
Sorrel Bri
745fd3b899 clean unused useState hooks from PCA component 2020-02-21 16:14:20 -08:00
Sorrel Bri
eb551a3fce refactor results reducer for cleanliness 2020-02-21 16:00:34 -08:00
Sorrel Bri
31fab0ea57 add support for multiple epochs with dependencies 2020-02-21 15:17:59 -08:00
Sorrel Bri
471759a7cf refactor rule decomposition for cleanliness 2020-02-20 17:06:13 -08:00
Sorrel Bri
6885aeba2f add error check for unknown tokens in feature portions of rules 2020-02-19 23:18:18 -08:00
Sorrel Bri
54dbe75a70 add error messages for rule syntax 2020-02-19 22:14:21 -08:00
Sorrel Bri
66be6e0650 confirm through fifth sample sound change rule 2020-02-19 18:52:29 -08:00
Sorrel Bri
162b6b8cfc debug word initial environment rules 2020-02-19 18:50:59 -08:00
Sorrel Bri
42f0b179c8 patch transformLexeme to remove empty phoneme object in case of deletion 2020-02-19 18:36:42 -08:00
Sorrel Bri
07be982b51 add support for word end and phoneme deletion 2020-02-19 18:33:00 -08:00
Sorrel Bri
a19102f82e display results of run in Output component 2020-02-16 18:28:43 -08:00
Sorrel Bri
3d0bde1c55 refactor results reducer for cleanliness 2020-02-16 15:11:36 -08:00
Sorrel Bri
afbd9d9bdf green on run for single epoch 2020-02-16 00:48:36 -08:00
Sorrel Bri
1b51405f02 patch decomposeRules in results to process phoneme sequences 2020-02-13 23:40:34 -08:00
Sorrel Bri
bcf79aa28c refactor features reducer for cleanliness 2020-02-13 21:02:51 -08:00
Sorrel Bri
298c153f88 rename reducer files to remove redundant state prefix 2020-02-13 20:20:03 -08:00
Sorrel Bri
1e15578aec map rules object on results run 2020-02-13 13:56:19 -08:00
Sorrel Bri
98864740ca hook Options form submit to RUN dispatch 2019-12-20 14:08:22 -08:00
Sorrel Bri
b748c8d104 hook results to Output render 2019-12-20 14:03:40 -08:00
Sorrel Bri
2d207c4783 patch failing SoundChangeSuite tests 2019-12-20 13:59:23 -08:00
Sorrel Bri
26d85eec4c hook SoundChangeSuite onChange events to dispatch SET_EPOCH 2019-12-20 13:33:43 -08:00
Sorrel Bri
252529feda hook Options output radio and save check onChange to dispatch SET_OPTIONS 2019-12-18 21:55:44 -08:00
Sorrel Bri
4ffeab701a add reducer action SET_OPTIONS 2019-12-18 21:49:55 -08:00
Sorrel Bri
77ebc5e1b9 hook options to Options render 2019-12-18 21:24:26 -08:00
Sorrel Bri
2c98a28624 hook remove epoch to dispatch REMOVE_EPOCH 2019-12-18 21:02:20 -08:00
Sorrel Bri
368b6ea1fa hook epochs into Epochs render 2019-12-18 20:11:21 -08:00
Sorrel Bri
9a8563ebd7 fix Features test, clear Features cruft 2019-12-18 15:51:41 -08:00
Sorrel Bri
5d98f91b0f patch feature display support for multi-graph phones 2019-12-18 15:38:22 -08:00
Sorrel Bri
cf0c971ee3 hook feature submit to reducer dispatch ADD_FEATURE 2019-12-17 19:10:48 -08:00
Sorrel Bri
5e0100edac hook features and phones into Features render 2019-12-16 22:22:37 -08:00
Sorrel Bri
f574e7b05f hook lexicon form onChange to reducer dispatch SET_LEXICON 2019-12-16 21:32:02 -08:00
Sorrel Bri
7de78839b0 hook lexicon into ProtoLang render 2019-12-16 14:52:38 -08:00
Sorrel Bri
cabf342b4a patch SET_EPOCH reducer action with index undefined check 2019-12-16 14:29:44 -08:00
Sorrel Bri
53c9abe317 Merge branch 'master' of https://github.com/sorrelbri/phono-change-applier 2019-12-16 13:43:01 -08:00
Sorrel
4c3bc4af0a
Delete report.20191129.153032.5120.0.001.json 2019-12-10 12:43:42 -08:00
82 changed files with 7637 additions and 1359 deletions

View file

@ -14,3 +14,7 @@
; all=true ; all=true
[strict] [strict]
[untyped]
.*\.scss
.*\.css

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Sorrel
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

112
README.md
View file

@ -1,3 +1,111 @@
# Phono Change Applier # Feature Change Applier
[Inspired by the Zompist Sound Change Applier 2](https://www.zompist.com/sca2.html) [Try the app!](https://sorrelbri.github.io/feature-change-applier/)
[Inspired by the Zompist Sound Change Applier 2](https://www.zompist.com/sca2.html)
## What is this?
Feature Change Applier is a tool for applying systemic sound change rules to an input lexicon.
Features:
- feature based phone definitions
- feature based sound change rule support
- multi-character phone support
- comparative runs for multiple rule sets
## What is LATL?
[Read the specification](/src/utils/latl/README.md)
LATL is a JavaScript targeting compiled language for doing linguistic analysis and transformations.
## How do I use FCA?
An FCA run requires the user to define three parameters:
- [the input lexicon](#The-Input-Lexicon), expressed in phonetic terms
- [the feature set](#the-feature-set), which maps each phonetic feature to positive or negative values for each phone
- [at least one 'epoch' of sound change rules](#epochs) to apply to the input lexicon
### The Input Lexicon
For best effect, the input lexicon should use a narrow phonetic transcription of each lexeme.
Lexemes must be separated by line breaks in order to be parsed properly by FCA.
Multi-word lexemes can be inserted with or without spaces, any white-space will be removed from the lexeme at runtime.
FCA does not currently support suprasegmentals by default, however features can be used to define prosodic information so long as it can be associated with a single phone.
For example:
- For tonemes, use IPA tone markers as in `ma˨˩˦` (马)
- For phonetic length, use IPA length markers as in `ħaːsin` (حَاسِن‎)
- For stress or syllabic breaks, however `ˈɡʊd.nɪs` may result in unpredictable behavior and is best avoided.
See below for defining these features in the feature set.
#### Future Changes to the Input Lexicon
- Future versions of FCA will allow for greater suprasegmental feature support.
- Future versions will allow for epoch specific lexemes
### The Feature Set
Phones in FCA are defined by the features they exhibit.
To add or edit a feature use the form to enter the feature name and the phones which are associated with the feature in the `+` and `-` inputs.
Phones should be separated by a forward slash and may be represented with multiple characters.
For example:
`aspirated + tʰ / pʰ / kʰ - t / p / k / ʔ`
Results in:
`[+ aspirated] = tʰ / pʰ / kʰ [- aspirated] = t / p / k / ʔ`
If the feature already exists, any phones associated with that feature will be replaced with the phones in the form.
A feature is not required to have a value for every phone, and every phone is not required to have a value for every feature.
Rules targeting `-` values for specific features will not target phones that are not defined in the feature set.
For example:
`[- aspirated]>ʔ/[+ vowel]_.`
This rule will not operate on the phone `ʊ` in `haʊs` as it was not defined with a negative `aspirated` value above.
#### Suprasegmentals
Toneme example using Mandarin tone system:
```
[+ tone] = ˥ / ˧˥ / ˨˩˦ / ˥˩ [- tone] =
[+ high] = ˥ / ˥˩ [- high] = ˧˥ / ˨˩˦
[+ low] = ˨˩˦ [- low] = ˥ / ˥˩ / ˧˥
[+ rising] = ˧˥ [- rising] = ˥ / ˨˩˦ / ˥˩
[+ falling] = ˨˩˦ / ˥˩ [- falling] = ˥ / ˧˥
```
Length example using Modern Standard Arabic (without allophonic variation):
```
[+ long] = aː / iː / uː [- long] = a / i / u / aj / aw
[+ geminate] = mː / nː / tː / tˤː / ... [- geminate] = m / n / t / tˤ / ...
```
#### Future Chagnes to the Feature Set
- Future versions of FCA will allow for greater suprasegmental feature support
- Future versions will allow for exclusive features. In the example below a phone cannot have a labial value and a coronal value simultaneously:
```
[!place
[labial
[+ labiodental] = f
[- labiodental] = p / m / kp / ŋm
[+ labiovelar] = kp / ŋm
[- labiovelar] = f / p / m
]
[coronal
[+ anterior] = t̪ / n̪ / t / n
[- anterior] = c / ɲ / ʈ / ɳ
[+ distributed] = t̪ / n̪ / c / ɲ
[- distributed] = t / n / ʈ / ɳ
]
...
]
```
### Epochs
This is where the rules to transform your lexicon are defined.
An FCA project can have as many 'epochs' or suites of sound change rules as you would like to define.
Rules can be defined using phones or features:
- `n>ŋ/._k`
- `[+ nasal alveolar]>[- alveolar + velar]/._[+ velar]`
These two rules will both act on the phone `n` in the sequence `nk` transforming it into `ŋ`, however the feature defined rule could also transform the `n` in the sequences `ng`, `nŋ`, `nx`, etc.
By default, FCA will pipe the initial lexicon into each one of these epochs and apply their transformations independently.
The output of one epoch can be piped into another epoch, however, by defining the `parent` parameter from the dropdown when adding a new epoch.

0
grammar.js Normal file
View file

2283
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,21 +1,30 @@
{ {
"name": "phono-change-applier", "name": "feature-change-applier",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"homepage": "https://sorrelbri.github.io/feature-change-applier",
"dependencies": { "dependencies": {
"flow-bin": "^0.113.0", "flow-bin": "^0.113.0",
"gh-pages": "^2.2.0",
"local-storage": "^2.0.0", "local-storage": "^2.0.0",
"node-sass": "^4.13.0", "moo": "^0.5.1",
"nearley": "^2.19.1",
"node-sass": "^4.13.1",
"react": "^16.12.0", "react": "^16.12.0",
"react-dom": "^16.12.0", "react-dom": "^16.12.0",
"react-scripts": "3.2.0" "react-router-dom": "^5.1.2",
"react-scripts": "^3.3.0"
}, },
"scripts": { "scripts": {
"start": "react-scripts start", "start": "react-scripts start",
"compile-grammar": "nearleyc src/utils/latl/grammar.ne -o src/utils/latl/grammar.js",
"test-grammar": "nearley-test src/utils/latl/grammar.js --input",
"flow": "flow", "flow": "flow",
"build": "react-scripts build", "build": "react-scripts build",
"test": "react-scripts test", "test": "react-scripts test",
"eject": "react-scripts eject" "eject": "react-scripts eject",
"predeploy": "npm run build",
"deploy": "gh-pages -d build"
}, },
"eslintConfig": { "eslintConfig": {
"extends": "react-app" "extends": "react-app"

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

BIN
public/assets/fca-demo.mov Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 318 B

View file

@ -5,7 +5,9 @@
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>Phono Change Applier</title> <link rel="stylesheet" href="%PUBLIC_URL%/stylesheets/reset.css">
<link href="https://fonts.googleapis.com/css?family=Catamaran|Fira+Code&display=swap" rel="stylesheet">
<title>Feature Change Applier</title>
</head> </head>
<body> <body>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>

88
public/latl/ipa.latl Normal file
View file

@ -0,0 +1,88 @@
set NASAL_PULMONIC_CONSONANTS = [ m̥, m, ɱ, n̼, n̥, n, ɳ̊, ɳ, ɲ̊, ɲ, ŋ, ̊ŋ, ɴ ],
STOP_PULMONIC_CONSONANTS = [ p, b, p̪, b̪, t̼, d̼, t, d, ʈ, ɖ, c, ɟ, k, ɡ, q, ɢ, ʡ, ʔ ],
S_FRICATIVE_PULMONIC_CONSONANTS = [ s, z, ʃ, ʒ, ʂ, ʐ, ɕ, ʑ ],
FRICATIVE_PULMONIC_CONSONANTS = [ ɸ, β, f, v, θ̼, ð̼, θ, ð, θ̠, ð̠, ɹ̠̊˔, ɹ̠˔, ɻ˔, ç, ʝ, x, ɣ, χ, ʁ, ħ, ʕ, h, ɦ ],
APPROXIMANT_PULMONIC_CONSONANTS = [ ʋ̥, ʋ, ɹ̥, ɹ, ɻ̊, ɻ, j̊, j, ɰ̊, ɰ, ʔ̞ ],
TAP_PULMONIC_CONSONANTS = [ ⱱ̟, ⱱ, ɾ̼, ɾ̥, ɾ, ɽ̊, ɽ, ɢ̆, ʡ̆ ],
TRILL_PULMONIC_CONSONANTS = [ ʙ̥, ʙ, r̥, r, ɽ̊r̥, ɽr, ʀ̥, ʀ, ʜ, ʢ ],
L_FRICATIVE_PULMONIC_CONSONANTS = [ ɬ, ɮ, ɭ̊˔, ɭ˔, ʎ̝̊, ʎ̝, ʟ̝̊, ʟ̝ ],
L_APPROXIMANT_PULMONIC_CONSONANTS = [ l̥, l, ɭ̊, ɭ, ʎ̥, ʎ, ʟ̥, ʟ, ʟ̠ ],
L_TAP_PULMONIC_CONSONANTS = [ ɺ, ɭ̆, ʎ̆, ʟ̆ ],
AFFRICATE_PULMONIC_CONSONANTS = [ pɸ, bβ, p̪f, b̪v, t̪θ, d̪ð, tɹ̝̊, dɹ̝, t̠ɹ̠̊˔, d̠ɹ̠˔, cç, ɟʝ, kx, ɡɣ, qχ, ʡʢ, ʔh ],
S_AFFRICATE_PULMONIC_CONSONANTS = [ ts, dz, t̠ʃ, d̠ʒ, ʈʂ, ɖʐ, tɕ, dʑ ],
L_AFFRICATE_PULMONIC_CONSONANTS = [ tɬ, dɮ, ʈɭ̊˔, cʎ̝̊, kʟ̝̊, ɡʟ̝ ],
DOUBLE_STOP_PULMONIC_CONSONANTS = [ t͡p, d͡b, k͡p, ɡ͡b, q͡ʡ ],
DOUBLE_NASAL_PULMONIC_CONSONANTS = [ n͡m, ŋ͡m ],
DOUBLE_FRICATIVE_PULMONIC_CONSONANTS = [ ɧ ],
DOUBLE_APPROXIMANT_PULMONIC_CONSONANTS = [ ʍ, w, ɥ̊, ɥ, ɫ ]
set PULMONIC_CONSONANTS, C = { NASAL_PULMONIC_CONSONANTS or STOP_PULMONIC_CONSONANTS
or S_FRICATIVE_PULMONIC_CONSONANTS or FRICATIVE_PULMONIC_CONSONANTS
or APPROXIMANT_PULMONIC_CONSONANTS or TAP_PULMONIC_CONSONANTS
or TRILL_PULMONIC_CONSONANTS or L_FRICATIVE_PULMONIC_CONSONANTS
or L_APPROXIMANT_PULMONIC_CONSONANTS or L_TAP_PULMONIC_CONSONANTS
or AFFRICATE_PULMONIC_CONSONANTS or S_AFFRICATE_PULMONIC_CONSONANTS
or L_AFFRICATE_PULMONIC_CONSONANTS or DOUBLE_STOP_PULMONIC_CONSONANTS
or DOUBLE_NASAL_PULMONIC_CONSONANTS or DOUBLE_FRICATIVE_PULMONIC_CONSONANTS
or DOUBLE_APPROXIMANT_PULMONIC_CONSONANTS
}
set STOP_EJECTIVE_CONSONANTS = [ pʼ, tʼ, ʈʼ, cʼ, kʼ, qʼ, ʡʼ ],
FRICATIVE_EJECTIVE_CONSONANTS = [ ɸʼ, fʼ, θʼ, sʼ, ʃʼ, ʂʼ, ɕʼ, xʼ, χʼ ],
L_FRICATIVE_EJECTIVE_CONSONANTS = [ ɬʼ ],
AFFRICATE_EJECTIVE_CONSONANTS = [ tsʼ, t̠ʃʼ, ʈʂʼ, kxʼ, qχʼ ],
L_AFFRICATE_EJECTIVE_CONSONANTS = [ tɬʼ, cʎ̝̊ʼ, kʟ̝̊ʼ ]
set EJECTIVE_CONSONANTS = { STOP_EJECTIVE_CONSONANTS or FRICATIVE_EJECTIVE_CONSONANTS
or L_FRICATIVE_EJECTIVE_CONSONANTS or AFFRICATE_EJECTIVE_CONSONANTS
or L_AFFRICATE_EJECTIVE_CONSONANTS
}
set TENUIS_CLICK_CONSONANTS = [ ʘ, ǀ, ǃ, ǂ ],
VOICED_CLICK_CONSONANTS = [ ʘ̬, ǀ̬, ǃ̬, ǂ̬ ],
NASAL_CLICK_CONSONANTS = [ ʘ̃, ǀ̃, ǃ̃, ǂ̃ ],
L_CLICK_CONSONANTS = [ ǁ, ǁ̬ ]
set CLICK_CONSONANTS = { TENUIS_CLICK_CONSONANTS or VOICED_CLICK_CONSONANTS
or NASAL_CLICK_CONSONANTS or L_CLICK_CONSONANTS
}
set IMPLOSIVE_CONSONANTS = [ ɓ, ɗ, ᶑ, ʄ, ɠ, ʛ, ɓ̥, ɗ̥, ᶑ̊, ʄ̊, ɠ̊, ʛ̥ ]
set NON_PULMONIC_CONSONANTS = { EJECTIVE_CONSONANTS or CLICK_CONSONANTS or IMPLOSIVE_CONSONANTS }
set CONSONANTS = { PULMONIC_CONSONANTS or NON_PULMONIC_CONSONANTS }
set MODAL_VOWELS = [ i, y, ɨ, ʉ, ɯ, u, ɪ, ʏ, ʊ, e, ø ɘ, ɵ ɤ, o, ø̞ ə, o̞, ɛ, œ ɜ, ɞ ʌ, ɔ, æ, ɐ, a, ɶ, ä, ɑ, ɒ ],
BREATHY_VOWELS = { [ V ] in MODAL_VOWELS yield [ V̤ ] },
VOICELESS_VOWELS = { [ V ] in MODAL_VOWELS yield [ V̥ ] },
CREAKY_VOWELS = { [ V ] in MODAL_VOWELS yield [ V̰ ] }
set SHORT_ORAL_VOWELS = { MODAL_VOWELS or BREATHY_VOWELS or CREAKY_VOWELS or VOICELESS_VOWELS },
LONG_ORAL_VOWELS = { [ V ] in SHORT_ORAL_VOWELS [ Vː ] },
ORAL_VOWELS = { SHORT_ORAL_VOWELS or LONG_ORAL_VOWELS }
set NASAL_VOWELS = { [ V ] in ORAL_VOWELS yield [ Ṽ ] },
SHORT_NASAL_VOWELS = { [ Vː ] in NASAL_VOWELS yield [ V ]ː },
LONG_NASAL_VOWELS = { [ Vː ] in NASAL_VOWELS }
set VOWELS = { ORAL_VOWELS or NASAL_VOWELS }
set PHONES = { VOWELS or CONSONANTS }
; print [ GLOBAL ]
[lateral
+=
L_AFFRICATE_EJECTIVE_CONSONANTS, L_AFFRICATE_PULMONIC_CONSONANTS, L_APPROXIMANT_PULMONIC_CONSONANTS,
L_CLICK_CONSONANTS, L_FRICATIVE_EJECTIVE_CONSONANTS, L_FRICATIVE_PULMONIC_CONSONANTS, L_TAP_PULMONIC_CONSONANTS
-=
{ not { [+ lateral ] in CONSONANTS } }, VOWELS
; alternative
; { not { [+ lateral ] in PHONES } }
]
*proto-lang
|child-lang

644
public/latl/waffle.latl Normal file
View file

@ -0,0 +1,644 @@
; -------- GA ENGLISH PHONETIC INVENTORY
; ---- VOWELS = æ / e / ə / ɑ / ɔ / ɪ̞ / ɛ / ʌ / ʊ̞ / i / u̟
; -- NASAL = æ̃ / ẽ / ə̃ / ɑ̃ / ɔ̃ / ɪ̃ / ɛ̃ / ʌ̃ / ʊ̃ / ĩ / ũ
; ɪ̞ / ʊ̞ = lowered
; u̟ = advanced
; -- LABIAL = u̟ / ʊ̞ / ɔ
; -- +HIGH = i / u̟ / ʊ̞ / ɪ̞
; -- -HIGH = ɑ / æ / e / ə / ɛ / ʌ
; -- +LOW = ɑ / æ / ɛ
; -- -LOW = i / u̟ / ʊ̞ / ɪ̞ / e / ə / ʌ
; -- +BACK = ɑ / ɔ / ʌ / ʊ̞ / u̟
; -- -BACK = æ / e / ə / ɪ̞ / ɛ / i
; -- +TENSE = e / i / u̟ / ɑ
; -- -TENSE = æ / ə / ɪ̞ / ɛ / ʌ / ʊ̞ / ɔ
; ---- DIPHTHONGS = eə / eɪ̯ / ju̟ / äɪ̞ / ɔɪ̞ / oʊ̞ / aʊ̞ / ɑɹ / iɹ / ɛɹ / ɔɹ / ʊɹ
; ---- CONSONANTS = p (pʰ) / b (b̥) / t (tʰ)(ɾ)(ʔ) / d (d̥)(ɾ) / tʃ / dʒ (d̥ʒ̊) / k (kʰ) / g (g̊) / f / v (v̥) / θ / ð (ð̥) /
; s / z (z̥) / ʃ / ʒ (ʒ̊) / h (ɦ)(ç) / m (ɱ)(m̩) / n(n̩) / ŋ / l (l̩)/ ɹ (ɹʲ ~ ɹˤ)(ɹ̩) / w (w̥) / j / x / ʔ
; -- PLOSIVES = p / p' / pʰ / t / t' / tʰ ɾ / k / k' / kʰ
; -- AFFRICATES = tʃ / dʒ
; -- FRICATIVES = f / v / θ / ð / s / z / ʃ / ʒ / ç / x
; -- NASAL OBSTRUENTS = m ɱ / n / ŋ
; -- LIQUIDS = l
; -- RHOTIC LIQUIDS = ɹ ɹʲ ɹˤ
; -- SYLLABIC CONSONANTS = m̩ / n̩ / l̩ / ɹ̩
; -- GLIDES = j / w
; -- LARYNGEALS = h ɦ / ʔ [- consonantal sonorant +/- LARYNGEAL FEATURES] only
; -------- distinctive groups
set PLOSIVES = [ p, pʰ, t, tʼ, tʰ, ɾ, kʼ, k, kʰ ]
AFFRICATES = [ tʃʰ, dʒ ]
FRICATIVES = [ f, v, θ, ð, s, z, ʃ, ʒ, ç, x ]
NASALS = [ m, ɱ, n, ŋ ]
LIQUIDS = [ l, ɹ, ɹʲ, ɹˤ ]
SYLLABICS = [ m̩, n̩, l̩, ɹ̩ ]
VOWELS = [ æ, e, ə, ɑ, ɔ, ɪ̞, ɛ, ʌ, ʊ̞, i, u̟ ]
GLIDES = [ j, w ]
LARYNGEALS = [ h, ɦ, ʔ ]
VOWELS = [ æ, e, ə, ɑ, ɔ, ɪ̞, ɛ, ʌ, ʊ̞, i, u̟ ]
; ---- implicit
; GLOBAL { all sets }
; ---- set join operations non-mutable!
; { SET_A not SET_B } left anti join
; { SET_A and SET_B } inner join
; { SET_A or SET_B } full outer join
; { not SET_A } = { GLOBAL not SET_A }
; ---- unnecessary sugar
; { not SET_A nor SET_B } = { GLOBAL not { SET_A or SET_B } }
; ---- set character operations - non-mutable!
; { [ Xy ] in SET_A } FILTER: where X is any character and y is a filtering character
; { SET_A yield [ Xy ] } CONCATENATE: performs transformation with (prepended or) appended character
; { SET_A yield [ X concat y ] }
; { SET_A yield [ y concat X ] }
; { SET_A yield y[ X ] } DISSOCIATE: performs transformation removing prepended (or appended) character
; { SET_A yield y dissoc [ X ] }
; { SET_A yield [ X ] dissoc y }
; { [ Xy ] in SET_A yield [ X ]y } combined FILTER and DISSOCIATE
; ---- TENTATIVE!
; ---- set feature operations - non-mutable!
; { [ + feature1 - feature2 ] in SET_A } FILTER: where feature1 and feature2 are filtering features
; { SET_A yield [ X + feature1 ] } TRANSFORMATION: performs transformation with (prepended or) appended character
; { SET_A yield [ X - feature1 ] }
; { SET_A yield [ X - feature1 + feature2 ] }
; { [ X + feature1 - feature2 ] in SET_A yield [ - feature1 + feature2 ] } combined FILTER and TRANSFORMATION
; ---- MAPPING
set PLOSIVES = [ p, t, k ],
FRICATIVES = [ f, s, x ],
; pairs PLOSIVES with FRICATIVES that have matching features = [ pf, ts, kx ]
AFFRICATES = { PLOSIVES yield [ X concat { [ [ X ] - fricative ] in FRICATIVES } ] }
; ---- example with join, character, and feature operations
; set SET_C = { [ PHONE +feature1 ] in { SET_A or SET_B } yield [ PHONE concat y ] }
; -------- main class features
[consonantal
+=
PLOSIVES, AFFRICATES, FRICATIVES, NASALS, LIQUIDS, SYLLABICS
-=
VOWELS, GLIDES, LARYNGEALS
]
[sonorant
+=
VOWELS, GLIDES, LIQUIDS, NASALS, SYLLABICS
-=
PLOSIVES, AFFRICATES, FRICATIVES, LARYNGEALS
]
[approximant
+=
VOWELS, LIQUIDS, GLIDES,
; SYLLABIC LIQUIDS
l̩, ɹ̩
-=
PLOSIVES, AFFRICATES, FRICATIVES, NASALS,
; SYLLABIC NASALS
m̩, n̩
]
; -------- laryngeal features
[voice
+=
VOWELS, GLIDES, LIQUIDS, NASALS, SYLLABICS,
; VOICED FRICATIVES
v, ð, z, ʒ,
; VOICED AFFRICATES
dʒ,
; VOICED LARYNGEALS
ɦ
-=
PLOSIVES,
; VOICELESS AFFRICATES
tʃ,
; VOICELESS FRICATIVES
f, θ, s, ʃ, ç, x,
; VOICELESS LARYNGEALS
h, ʔ
]
[spreadGlottis
+=
; ASPIRATED PLOSIVES
pʰ, tʰ, kʰ,
; ASPIRATED AFFRICATES
; SPREAD LARYNGEALS
h ɦ
-=
VOWELS, FRICATIVES, NASALS, LIQUIDS, SYLLABICS, GLIDES,
; UNASPIRATED PLOSIVES
p, pʼ, t, tʼ, ɾ, k, kʼ,
; UNASPIRATED AFFRICATES
tʃ, dʒ,
; CONSTRICTED LARYNGEALS
ʔ
]
[constrictedGlottis
+=
; LARYNGEALIZED RHOTIC
ɹˤ,
; CONSTRICTED LARYNGEAL
ʔ,
; EJECTIVE PLOSIVES
pʼ, tʼ, kʼ
-=
VOWELS, AFFRICATES, FRICATIVES, NASALS, SYLLABICS, GLIDES,
; UNCONSTRICTED PLOSIVES
{ PLOSIVES not [ p', t', k' ] },
; NON-CONSTRICTED LIQUIDS
l, ɹ ɹʲ,
; SPREAD LARYNGEALS
h ɦ,
]
; -------- manner features
[continuant
+=
; FRICATIVES
f, v, θ, ð, s, z, ʃ, ʒ, ç, x,
; VOWELS
æ, e, ə, ɑ, ɔ, ɪ̞, ɛ, ʌ, ʊ̞, i, u̟, æ̃, ẽ, ə̃, ɑ̃, ɔ̃, ɪ̃, ɛ̃, ʌ̃, ʊ̃, ĩ, ũ
; LIQUIDS + RHOTICS
l, ɹ ɹʲ ɹˤ,
; GLIDES
j, w,
; SYLLABIC LIQUIDS
l̩, ɹ̩,
; TAPS
ɾ
-=
; NON-TAP PLOSIVES
p, pʼ, pʰ, t, tʼ, tʰ, k, kʼ, kʰ,
; AFFRICATES
tʃ, dʒ,
; NASALS
m ɱ, n, ŋ,
; SYLLABIC NASALS
m̩, n̩
]
[nasal
+=
; NASALS
m ɱ, n, ŋ,
; SYLLABIC NASALS
m̩, n̩
-=
; VOWELS
æ, e, ə, ɑ, ɔ, ɪ̞, ɛ, ʌ, ʊ̞, i, u̟, æ̃, ẽ, ə̃, ɑ̃, ɔ̃, ɪ̃, ɛ̃, ʌ̃, ʊ̃, ĩ, ũ
; FRICATIVES
f, v, θ, ð, s, z, ʃ, ʒ, ç, x,
; LIQUIDS + RHOTICS
l, ɹ ɹʲ ɹˤ,
; GLIDES
j, w,
; SYLLABIC LIQUIDS
l̩, ɹ̩,
; PLOSIVES
p, pʼ, pʰ, t, tʼ, tʰ ɾ, k, kʼ, kʰ,
; AFFRICATES
tʃ, dʒ,
]
[strident
+=
; STRIDENT FRICATIVES
f, v, s, z, ʃ, ʒ,
; STRIDENT AFFRICATES
tʃ, dʒ
-=
; VOWELS
æ̃, ẽ, ə̃, ɑ̃, ɔ̃, ɪ̃, ɛ̃, ʌ̃, ʊ̃, ĩ, ũ
; PLOSIVES
p, pʼ, pʰ, t, tʼ, tʰ ɾ, k, kʼ, kʰ,
; NON-STRIDENT FRICATIVES
θ, ð, ç, x,
; NASAL OBSTRUENTS
m ɱ, n, ŋ,
; RHOTICS + LIQUIDS
l, ɹ ɹʲ ɹˤ,
; SYLLABIC CONSONANTS
m̩, n̩, l̩, ɹ̩,
; GLIDES
j, w
]
[lateral
+=
; LATERAL LIQUIDS
l,
; SYLLABIC LATERALS,
-=
; VOWELS
æ, e, ə, ɑ, ɔ, ɪ̞, ɛ, ʌ, ʊ̞, i, u̟, æ̃, ẽ, ə̃, ɑ̃, ɔ̃, ɪ̃, ɛ̃, ʌ̃, ʊ̃, ĩ, ũ
; PLOSIVES
p, pʼ, pʰ, t, tʼ, tʰ ɾ, k, kʼ, kʰ
; AFFRICATES
tʃ, dʒ
; FRICATIVES
f, v, θ, ð, s, z, ʃ, ʒ, ç, x
; NASAL OBSTRUENTS
m ɱ, n, ŋ
; RHOTIC LIQUIDS
ɹ ɹʲ ɹˤ
; NON-LIQUID SYLLABIC CONSONANTS
m̩, n̩, ɹ̩
; GLIDES
j, w
]
; -------- ---- PLACE features
; -------- labial features
[labial
+=
; ROUNDED VOWELS
u̟, ʊ̞, ɔ, ʊ̃, ũ, ɔ̃
; LABIAL PLOSIVES
p, pʼ, pʰ,
; LABIAL FRICATIVES
f, v,
; LABIAL NASALS
m ɱ,
; LABIAL SYLLABIC CONSONANTS
m̩,
; LABIAL GLIDES
w
-=
; UNROUNDED VOWELS
æ, e, ə, ɑ, ɪ̞, ɛ, ʌ, i, æ̃, ẽ, ə̃, ɑ̃, ɪ̃, ɛ̃, ʌ̃, ĩ,
; NON-LABIAL PLOSIVES
t, tʼ, tʰ ɾ, k, kʼ, kʰ,
; NON-LABIAL AFFRICATES
tʃ, dʒ,
; NON-LABIAL FRICATIVES
θ, ð, s, z, ʃ, ʒ, ç, x,
; NON-LABIAL NASAL OBSTRUENTS
n, ŋ,
; LIQUIDS
l,
; RHOTIC LIQUIDS
ɹ ɹʲ ɹˤ,
; NON-LABIAL SYLLABIC CONSONANTS
n̩, l̩, ɹ̩,
; NON-LABIAL GLIDES
j
]
; -------- coronal features
[coronal
+=
; CORONAL PLOSIVES
t, tʼ, tʰ ɾ,
; CORONAL AFFRICATES
tʃ, dʒ,
; CORONAL FRICATIVES
θ, ð, s, z, ʃ, ʒ,
; CORONAL NASALS
n,
; CORONAL LIQUIDS
l
; CORONAL RHOTIC LIQUIDS
ɹ
; CORONAL SYLLABIC CONSONANTS
n̩, l̩, ɹ̩
-=
; VOWELS
æ, e, ə, ɑ, ɔ, ɪ̞, ɛ, ʌ, ʊ̞, i, u̟, æ̃, ẽ, ə̃, ɑ̃, ɔ̃, ɪ̃, ɛ̃, ʌ̃, ʊ̃, ĩ, ũ
; NON-CORONAL PLOSIVES
p, pʼ, pʰ, k, kʼ, kʰ
; NON-CORONAL FRICATIVES
f, v, ç, x
; NON-CORONAL NASAL OBSTRUENTS
m ɱ, ŋ
; NON-CORONAL RHOTIC LIQUIDS
ɹʲ ɹˤ
; NON-CORONAL SYLLABIC CONSONANTS
m̩,
; NON-CORONAL GLIDES
j, w
]
[anterior
+=
; ALVEOLAR PLOSIVES
t, tʼ, tʰ ɾ,
; ALVEOLAR AFFRICATES
tʃ, dʒ,
; DENTAL FRICATIVES
θ, ð,
; ALVEOLAR FRICATIVES
s, z,
; ALVEOLAR NASALS
n,
; ALVEOLAR LIQUIDS
l
; ALVEOLAR SYLLABIC CONSONANTS
n̩, l̩,
-=
; POSTALVEOLAR FRICATIVES
ʃ, ʒ,
; POSTALVEOLAR RHOTIC LIQUIDS
ɹ,
; POSTALVEOLAR SYLLABIC CONSONANTS
ɹ̩,
; -- NON-CORONALs
; VOWELS
æ, e, ə, ɑ, ɔ, ɪ̞, ɛ, ʌ, ʊ̞, i, u̟, æ̃, ẽ, ə̃, ɑ̃, ɔ̃, ɪ̃, ɛ̃, ʌ̃, ʊ̃, ĩ, ũ
; NON-CORONAL PLOSIVES
p, pʼ, pʰ, k, kʼ, kʰ
; NON-CORONAL FRICATIVES
f, v, ç, x
; NON-CORONAL NASAL OBSTRUENTS
m ɱ, ŋ
; NON-CORONAL RHOTIC LIQUIDS
ɹʲ ɹˤ
; NON-CORONAL SYLLABIC CONSONANTS
m̩,
; NON-CORONAL GLIDES
j, w
]
[distributed
+=
; DENTAL FRICATIVES
θ, ð,
; POSTALVEOLAR FRICATIVES
ʃ, ʒ,
; POSTALVEOLAR RHOTIC LIQUIDS
ɹ,
; POSTALVEOLAR SYLLABIC CONSONANTS
ɹ̩,
-=
; apical, retroflex
; ALVEOLAR PLOSIVES
t, tʼ, tʰ ɾ,
; ALVEOLAR FRICATIVES
s, z,
; ALVEOLAR NASALS
n,
; ALVEOLAR LIQUIDS
l
; ALVEOLAR SYLLABIC CONSONANTS
n̩, l̩,
; -- NON-CORONALS
; VOWELS
æ, e, ə, ɑ, ɔ, ɪ̞, ɛ, ʌ, ʊ̞, i, u̟, æ̃, ẽ, ə̃, ɑ̃, ɔ̃, ɪ̃, ɛ̃, ʌ̃, ʊ̃, ĩ, ũ
; NON-CORONAL PLOSIVES
p, pʼ, pʰ, k, kʼ, kʰ
; NON-CORONAL FRICATIVES
f, v, ç, x
; NON-CORONAL NASAL OBSTRUENTS
m ɱ, ŋ
; NON-CORONAL RHOTIC LIQUIDS
ɹʲ ɹˤ
; NON-CORONAL SYLLABIC CONSONANTS
m̩,
; NON-CORONAL GLIDES
j, w
]
; -------- dorsal features
[dorsal
+=
; VOWELS
æ, e, ə, ɑ, ɔ, ɪ̞, ɛ, ʌ, ʊ̞, i, u̟, æ̃, ẽ, ə̃, ɑ̃, ɔ̃, ɪ̃, ɛ̃, ʌ̃, ʊ̃, ĩ, ũ
; DORSAL PLOSIVES
k, kʼ, kʰ,
; DORSAL FRICATIVES
ç, x,
; DORSAL NASAL OBSTRUENTS
ŋ,
; DORSAL RHOTIC LIQUIDS
ɹʲ ɹˤ
; DORSAL GLIDES
j
-=
; NON-DORSAL PLOSIVES
p, pʼ, pʰ, t, tʼ, tʰ ɾ,
; NON-DORSAL AFFRICATES
tʃ, dʒ,
; NON-DORSAL FRICATIVES
f, v, θ, ð, s, z, ʃ, ʒ,
; NON-DORSAL NASALS
m ɱ, n,
; NON-DORSAL LIQUIDS
l
; NON-DORSAL RHOTIC LIQUIDS
ɹ
; NON-DORSAL SYLLABIC CONSONANTS
m̩, n̩, l̩, ɹ̩
; NON-DORSAL GLIDES
w
]
[high
+=
; HIGH VOWELS
i, u̟, ʊ̞, ɪ̞, ĩ, ũ, ʊ̃, ɪ̃
; HIGH DORSAL PLOSIVES
k, kʼ, kʰ,
; HIGH DORSAL FRICATIVES
ç, x,
; HIGH DORSAL NASAL OBSTRUENTS
ŋ,
; HIGH RHOTIC LIQUIDS
ɹʲ
; HIGH DORSAL GLIDES
j, w
-= χ, e, o, a
; NON-HIGH VOWELS
ɑ, æ, e, ə, ɛ, ʌ, æ̃, ẽ, ə̃, ɑ̃, ɔ̃, ɛ̃, ʌ̃,
; NON-HIGH RHOTIC LIQUIDS
ɹˤ
; -- NON-DORSALS
; NON-DORSAL PLOSIVES
p, pʼ, pʰ, t, tʼ, tʰ ɾ,
; NON-DORSAL AFFRICATES
tʃ, dʒ,
; NON-DORSAL FRICATIVES
f, v, θ, ð, s, z, ʃ, ʒ,
; NON-DORSAL NASALS
m ɱ, n,
; NON-DORSAL LIQUIDS
l
; NON-DORSAL RHOTIC LIQUIDS
ɹ
; NON-DORSAL SYLLABIC CONSONANTS
m̩, n̩, l̩, ɹ̩
; NON-DORSAL GLIDES
w
]
[low
+=
; LOW VOWELS
ɑ, æ, ɛ, æ̃, ɑ̃, ɛ̃,
; LOW DORSAL RHOTIC LIQUIDS
ɹˤ
-= a, ɛ, ɔ
; NON-LOW VOWELS
i, u̟, ʊ̞, ɪ̞, e, ə, ʌ, ẽ, ə̃, ɔ̃, ɪ̃, ʌ̃, ʊ̃, ĩ, ũ
; NON-LOW DORSAL PLOSIVES
k, kʼ, kʰ,
; NON-LOW DORSAL FRICATIVES
ç, x,
; NON-LOW DORSAL NASAL OBSTRUENTS
ŋ,
; NON-LOW DORSAL RHOTIC LIQUIDS
ɹʲ
; DORSAL GLIDES
j
; -- NON-DORSALS
; NON-DORSAL PLOSIVES
p, pʼ, pʰ, t, tʼ, tʰ ɾ,
; NON-DORSAL AFFRICATES
tʃ, dʒ,
; NON-DORSAL FRICATIVES
f, v, θ, ð, s, z, ʃ, ʒ,
; NON-DORSAL NASALS
m ɱ, n,
; NON-DORSAL LIQUIDS
l
; NON-DORSAL RHOTIC LIQUIDS
ɹ
; NON-DORSAL SYLLABIC CONSONANTS
m̩, n̩, l̩, ɹ̩
; NON-DORSAL GLIDES
w
]
[back
+=
; k, kʼ, ɣ, χ, u, ə, o, ʌ, ɑ
; BACK VOWELS
ɑ, ɔ, ʌ, ʊ̞, u̟, ɑ̃, ɔ̃, ʌ̃, ʊ̃, ũ,
; BACK DORSAL PLOSIVES
k, kʼ, kʰ,
; BACK DORSAL FRICATIVES
x,
; BACK DORSAL NASAL OBSTRUENTS
ŋ,
; BACK DORSAL RHOTIC LIQUIDS
ɹˤ
-= ç, k̟, i, y, ø, ɛ
; NON-BACK DORSAL FRICATIVES
ç,
; NON-BACK DORSAL RHOTIC LIQUIDS
ɹʲ
; NON-BACK DORSAL GLIDES
j
; NON-BACK VOWELS
æ, e, ə, ɪ̞, ɛ, i, æ̃, ẽ, ə̃, ɪ̃, ɛ̃, ĩ
; -- NON-DORSALS
; NON-DORSAL PLOSIVES
p, pʼ, pʰ, t, tʼ, tʰ ɾ,
; NON-DORSAL AFFRICATES
tʃ, dʒ,
; NON-DORSAL FRICATIVES
f, v, θ, ð, s, z, ʃ, ʒ,
; NON-DORSAL NASALS
m ɱ, n,
; NON-DORSAL LIQUIDS
l
; NON-DORSAL RHOTIC LIQUIDS
ɹ
; NON-DORSAL SYLLABIC CONSONANTS
m̩, n̩, l̩, ɹ̩
; NON-DORSAL GLIDES
w
]
[tense ; compare to ATR or RTR
+=
; TENSE VOWELS
e, i, u̟, ɑ, ĩ, ũ, ẽ, ɑ̃,
-=
; NON-TENSE VOWELS
æ, ə, ɪ̞, ɛ, ʌ, ʊ̞, ɔ, æ̃, ə̃, ɔ̃, ɪ̃, ɛ̃, ʌ̃, ʊ̃,
; DORSAL PLOSIVES
k, kʼ, kʰ,
; DORSAL FRICATIVES
ç, x,
; DORSAL NASAL OBSTRUENTS
ŋ,
; DORSAL RHOTIC LIQUIDS
ɹʲ ɹˤ,
; DORSAL GLIDES
j
; -- NON-DORSALS
; NON-DORSAL PLOSIVES
p, pʼ, pʰ, t, tʼ, tʰ ɾ,
; NON-DORSAL AFFRICATES
tʃ, dʒ,
; NON-DORSAL FRICATIVES
f, v, θ, ð, s, z, ʃ, ʒ,
; NON-DORSAL NASALS
m ɱ, n,
; NON-DORSAL LIQUIDS
l
; NON-DORSAL RHOTIC LIQUIDS
ɹ
; NON-DORSAL SYLLABIC CONSONANTS
m̩, n̩, l̩, ɹ̩
; NON-DORSAL GLIDES
w
]
*PROTO
|Gif Lang
*PROTO
|Jif Lang
; -- Devoicing, all our z's become s's
[ + voice consonantal - nasal]>[- voice]/._.
; -- loss of schwa, the is th'
ə>0/._.
; -- Ejectivization, all our pits become pit's
[+ spreadGlottis - continuant]>[+ constrictedGlottis - spreadGlottis]/._[+ constrictedGlottis]
[+ spreadGlottis - continuant]>[+ constrictedGlottis - spreadGlottis]/[+ constrictedGlottis]_.
[+ constrictedGlottis]>0/[+ constrictedGlottis - continuant]_.
[+ constrictedGlottis]>0/._[+ constrictedGlottis - continuant]
; -- r color spreading, all our reports become rihpahts
[- consonantal tense]>[+ tense]/ɹ_.
[- consonantal tense]>[+ tense]/._ɹ
[- consonantal high]>[+ high]/ɹʲ_.
[- consonantal high]>[+ high]/._ɹʲ
[- consonantal back]>[+ back]/ɹˤ_.
[- consonantal back]>[+ back]/._ɹˤ
ɹ>0/._.
ɹʲ>0/._.
ɹˤ>0/._.
; -- Deaspiration, tiff is diff and diff is tiff
[+ spreadGlottis - continuant]>[- spreadGlottis]/._.
; "JavaScript"
; "gif or jif? I say zhaif"
; "This request returns an empty object"
; "I love going to waffle js!"
; "A donut a day makes living with the threat of pandemic easier"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

View file

@ -1,21 +1,11 @@
{ {
"short_name": "React App", "short_name": "FCA",
"name": "Create React App Sample", "name": "Feature Change Applier",
"icons": [ "icons": [
{ {
"src": "favicon.ico", "src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16", "sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon" "type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
} }
], ],
"start_url": ".", "start_url": ".",

View file

@ -0,0 +1,7 @@
$colors: (
"main--bg": #281734,
"main": #d5bfbf,
"text-input": #e8e22e,
"text-input--bg": #1d191a,
"error": #ff0000
);

View file

@ -0,0 +1,366 @@
/* http://meyerweb.com/eric/tools/css/reset/
v2.0-modified | 20110126
License: none (public domain)
*/
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* make sure to set some focus styles for accessibility */
:focus {
outline: 0;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
display: block;
}
body {
line-height: 1;
}
ol, ul {
list-style: none;
}
blockquote, q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
input[type=search]::-webkit-search-cancel-button,
input[type=search]::-webkit-search-decoration,
input[type=search]::-webkit-search-results-button,
input[type=search]::-webkit-search-results-decoration {
-webkit-appearance: none;
-moz-appearance: none;
}
input[type=search] {
-webkit-appearance: none;
-moz-appearance: none;
-webkit-box-sizing: content-box;
-moz-box-sizing: content-box;
box-sizing: content-box;
}
textarea {
overflow: auto;
vertical-align: top;
resize: vertical;
}
/**
* Correct `inline-block` display not defined in IE 6/7/8/9 and Firefox 3.
*/
audio,
canvas,
video {
display: inline-block;
*display: inline;
*zoom: 1;
max-width: 100%;
}
/**
* Prevent modern browsers from displaying `audio` without controls.
* Remove excess height in iOS 5 devices.
*/
audio:not([controls]) {
display: none;
height: 0;
}
/**
* Address styling not present in IE 7/8/9, Firefox 3, and Safari 4.
* Known issue: no IE 6 support.
*/
[hidden] {
display: none;
}
/**
* 1. Correct text resizing oddly in IE 6/7 when body `font-size` is set using
* `em` units.
* 2. Prevent iOS text size adjust after orientation change, without disabling
* user zoom.
*/
html {
font-size: 100%; /* 1 */
-webkit-text-size-adjust: 100%; /* 2 */
-ms-text-size-adjust: 100%; /* 2 */
}
/**
* Address `outline` inconsistency between Chrome and other browsers.
*/
a:focus {
outline: thin dotted;
}
/**
* Improve readability when focused and also mouse hovered in all browsers.
*/
a:active,
a:hover {
outline: 0;
}
/**
* 1. Remove border when inside `a` element in IE 6/7/8/9 and Firefox 3.
* 2. Improve image quality when scaled in IE 7.
*/
img {
border: 0; /* 1 */
-ms-interpolation-mode: bicubic; /* 2 */
}
/**
* Address margin not present in IE 6/7/8/9, Safari 5, and Opera 11.
*/
figure {
margin: 0;
}
/**
* Correct margin displayed oddly in IE 6/7.
*/
form {
margin: 0;
}
/**
* Define consistent border, margin, and padding.
*/
fieldset {
border: 1px solid #c0c0c0;
margin: 0 2px;
padding: 0.35em 0.625em 0.75em;
}
/**
* 1. Correct color not being inherited in IE 6/7/8/9.
* 2. Correct text not wrapping in Firefox 3.
* 3. Correct alignment displayed oddly in IE 6/7.
*/
legend {
border: 0; /* 1 */
padding: 0;
white-space: normal; /* 2 */
*margin-left: -7px; /* 3 */
}
/**
* 1. Correct font size not being inherited in all browsers.
* 2. Address margins set differently in IE 6/7, Firefox 3+, Safari 5,
* and Chrome.
* 3. Improve appearance and consistency in all browsers.
*/
button,
input,
select,
textarea {
font-size: 100%; /* 1 */
margin: 0; /* 2 */
vertical-align: baseline; /* 3 */
*vertical-align: middle; /* 3 */
}
/**
* Address Firefox 3+ setting `line-height` on `input` using `!important` in
* the UA stylesheet.
*/
button,
input {
line-height: normal;
}
/**
* Address inconsistent `text-transform` inheritance for `button` and `select`.
* All other form control elements do not inherit `text-transform` values.
* Correct `button` style inheritance in Chrome, Safari 5+, and IE 6+.
* Correct `select` style inheritance in Firefox 4+ and Opera.
*/
button,
select {
text-transform: none;
}
/**
* 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
* and `video` controls.
* 2. Correct inability to style clickable `input` types in iOS.
* 3. Improve usability and consistency of cursor style between image-type
* `input` and others.
* 4. Remove inner spacing in IE 7 without affecting normal text inputs.
* Known issue: inner spacing remains in IE 6.
*/
button,
html input[type="button"], /* 1 */
input[type="reset"],
input[type="submit"] {
-webkit-appearance: button; /* 2 */
cursor: pointer; /* 3 */
*overflow: visible; /* 4 */
}
/**
* Re-set default cursor for disabled elements.
*/
button[disabled],
html input[disabled] {
cursor: default;
}
/**
* 1. Address box sizing set to content-box in IE 8/9.
* 2. Remove excess padding in IE 8/9.
* 3. Remove excess padding in IE 7.
* Known issue: excess padding remains in IE 6.
*/
input[type="checkbox"],
input[type="radio"] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
*height: 13px; /* 3 */
*width: 13px; /* 3 */
}
/**
* 1. Address `appearance` set to `searchfield` in Safari 5 and Chrome.
* 2. Address `box-sizing` set to `border-box` in Safari 5 and Chrome
* (include `-moz` to future-proof).
*/
input[type="search"] {
-webkit-appearance: textfield; /* 1 */
-moz-box-sizing: content-box;
-webkit-box-sizing: content-box; /* 2 */
box-sizing: content-box;
}
/**
* Remove inner padding and search cancel button in Safari 5 and Chrome
* on OS X.
*/
input[type="search"]::-webkit-search-cancel-button,
input[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* Remove inner padding and border in Firefox 3+.
*/
button::-moz-focus-inner,
input::-moz-focus-inner {
border: 0;
padding: 0;
}
/**
* 1. Remove default vertical scrollbar in IE 6/7/8/9.
* 2. Improve readability and alignment in all browsers.
*/
textarea {
overflow: auto; /* 1 */
vertical-align: top; /* 2 */
}
/**
* Remove most spacing between table cells.
*/
table {
border-collapse: collapse;
border-spacing: 0;
}
html,
button,
input,
select,
textarea {
color: #222;
}
::-moz-selection {
background: #b3d4fc;
text-shadow: none;
}
::selection {
background: #b3d4fc;
text-shadow: none;
}
img {
vertical-align: middle;
}
fieldset {
border: 0;
margin: 0;
padding: 0;
}
textarea {
resize: vertical;
}
.chromeframe {
margin: 0.2em 0;
background: #ccc;
color: #000;
padding: 0.2em 0;
}

View file

@ -7,14 +7,12 @@
} }
.App-header { .App-header {
background-color: #282c34;
min-height: 100vh; min-height: 100vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: calc(10px + 2vmin); font-size: calc(10px + 2vmin);
color: white;
} }
.App-link { .App-link {

View file

@ -5,7 +5,7 @@ import PhonoChangeApplier from './PhonoChangeApplier';
function App() { function App() {
return ( return (
<div className="App" data-testid="App"> <div className="App" data-testid="App">
<h1>Phono Change Applier</h1> <h1 data-testid="App-name">Feature Change Applier</h1>
<PhonoChangeApplier /> <PhonoChangeApplier />
</div> </div>
); );

View file

@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { HashRouter as Router } from 'react-router-dom';
import App from './App'; import App from './App';
import renderer from 'react-test-renderer'; import renderer from 'react-test-renderer';
import { exportAllDeclaration } from '@babel/types'; import { exportAllDeclaration } from '@babel/types';
@ -8,13 +9,13 @@ import extendExpect from '@testing-library/jest-dom/extend-expect'
it('renders App without crashing', () => { it('renders App without crashing', () => {
const div = document.createElement('div'); const div = document.createElement('div');
ReactDOM.render(<App />, div); ReactDOM.render(<Router><App /></Router>, div);
ReactDOM.unmountComponentAtNode(div); ReactDOM.unmountComponentAtNode(div);
}); });
describe('App', () => { describe('App', () => {
it('renders the correct title', () => { it('renders the correct title', () => {
const { getByTestId } = render(<App />); const { getByTestId } = render(<Router><App /></Router>);
expect(getByTestId('App')).toHaveTextContent('Phono Change Applier'); expect(getByTestId('App-name')).toHaveTextContent('Feature Change Applier');
}) })
}) })

View file

@ -1,7 +1,7 @@
import React, { useState, useReducer } from 'react'; import React, { useState, useReducer } from 'react';
import './PhonoChangeApplier.scss'; import { Link, Route } from 'react-router-dom';
// import ls from 'local-storage'; import './PhonoChangeApplier.scss';
import ProtoLang from './components/ProtoLang'; import ProtoLang from './components/ProtoLang';
import Features from './components/Features'; import Features from './components/Features';
@ -9,125 +9,43 @@ import Epochs from './components/Epochs';
import Options from './components/Options'; import Options from './components/Options';
import Output from './components/Output'; import Output from './components/Output';
import {stateReducer} from './reducers/stateReducer'; import Latl from './components/Latl';
import {initState} from './reducers/stateReducer.init'; import LatlOutput from './components/LatlOutput';
import { stateReducer } from './reducers/reducer';
import { clearState, waffleState } from './reducers/reducer.init';
const PhonoChangeApplier = () => { const PhonoChangeApplier = () => {
const [ state, dispatch ] = useReducer( const [ state, dispatch ] = useReducer(
stateReducer, stateReducer,
{}, {},
initState waffleState
) )
const { lexicon, phones, phonemes, epochs, options, features, results, errors, latl, parseResults } = state;
const [ lexicon, setLexicon ] = useState(['mun', 'tʰu', 'tɯm', 'utʰ']);
const [ phonemes, setPhonemes ] = useState(
// ! candidate for trie to avoid situations where >2 graph phonemes
// ! are uncaught by lexeme decomposition when <n graph phonemes are not present
{
n: [ 'occlusive', 'sonorant', 'obstruent', 'nasal', 'alveolar' ],
m: [ 'occlusive', 'sonorant', 'obstruent', 'nasal', 'bilabial' ],
u: [ 'continuant', 'sonorant', 'syllabic', 'high', 'back', 'rounded' ],
ɯ: [ 'continuant', 'sonorant', 'syllabic', 'high', 'back', 'unrounded' ],
t: [ 'occlusive', 'plosive', 'obstruent', 'alveolar' ],
: [ 'occlusive', 'plosive', 'obstruent', 'alveolar', 'aspirated' ],
}
);
const [ epochs, setEpochs ] = useState([{name: 'epoch 1', changes:['[+ rounded]>[- rounded + unrounded]/_#']}]);
const [ options, setOptions ] = useState({output: 'default', save: false})
const [ results, setResults ] = useState([])
const [ errors, setErrors ] = useState({})
const [ features, setFeatures ] = useState(
['occlusive', 'sonorant', 'obstruent', 'nasal', 'alveolar','bilabial',
'continuant','syllabic','high','back','rounded','unrounded', 'plosive','aspirated'])
const runChanges = e => {
e.preventDefault();
let ruleError = epochs.reduce((errorObject, epoch) => {
epoch.changes.map((change, index) => {
if (!change.match(/>.*\/.*_/)) {
errorObject[epoch.name]
? errorObject[epoch.name].push(index)
: errorObject[epoch.name] = [index]
errorObject[epoch.name].ruleSyntaxError = true;
}
// TODO validate phoneme syntax
let decomposedChange = change.split('>');
decomposedChange = [decomposedChange[0], ...decomposedChange[1].split('/')]
decomposedChange = [decomposedChange[0], decomposedChange[1], ...decomposedChange[2].split('_')];
})
return errorObject;
}, {})
if (Object.entries(ruleError).length) return setErrors(ruleError)
setErrors({});
// decompose Lexical Items
// moving window on phonemes of each lexical item
let lexicalFeatureBundles = []
lexicon.forEach(lexeme => {
let lexemeBundle = [];
let startingIndex = 0;
let lastIndex = lexeme.length - 1;
[...lexeme].forEach((_, index) => {
if (phonemes[lexeme.slice(startingIndex, index + 1)] && index !== lastIndex) return;
if (phonemes[lexeme.slice(startingIndex, index + 1)]) return lexemeBundle.push(phonemes[lexeme.slice(startingIndex)])
if (index !== 0 && index !== lastIndex) lexemeBundle.push(phonemes[lexeme.slice(startingIndex, index)])
if (index === lastIndex) {
lexemeBundle.push(phonemes[lexeme.slice(startingIndex, index)])
lexemeBundle.push(phonemes[lexeme.slice(index)])
}
startingIndex = index;
})
lexemeBundle.unshift(['#'])
lexemeBundle.push(['#'])
lexicalFeatureBundles.push(lexemeBundle);
})
console.log(lexicalFeatureBundles)
// decompose rules
let allEpochs = epochs.map(epoch => {
let ruleBundle = epoch.changes.map(rule => {
return {
input: rule.split('>')[0].replace(/\[|\]|\+/g, '').trim(),
result: rule.split('>')[1].split('/')[0],
preInput: rule.split('/')[1].split('_')[0].replace(/\[|\]|\+/g, '').trim(),
postInput: rule.split('/')[1].split('_')[1].replace(/\[|\]|\+/g, '').trim(),
}
})
return {epoch: epoch.name, rules: ruleBundle}
})
console.log(allEpochs)
// apply sound changes
allEpochs.reduce((diachronicLexicon, epoch) => {
let startingLexicon = diachronicLexicon.length
? diachronicLexicon[diachronicLexicon.length - 1]
: lexicalFeatureBundles;
let currentRules = epoch.rules;
let resultingLexicon = startingLexicon.forEach(lexeme => {
currentRules.forEach(rule => {
let ruleEnvironment = [[rule.preInput], [rule.input], [rule.postInput]];
console.log(ruleEnvironment)
})
})
diachronicLexicon.push(resultingLexicon)
},[])
// handle output
}
return ( return (
<div className="PhonoChangeApplier" data-testid="PhonoChangeApplier"> <>
<ProtoLang lexicon={lexicon} setLexicon={setLexicon}/>
<Features phonemes={phonemes} setPhonemes={setPhonemes} features={features} setFeatures={setFeatures}/> <Route exact path="/latl">
<Epochs epochs={epochs} setEpochs={setEpochs} errors={errors}/> <Link to="/">Back to GUI</Link>
<Options options={options} setOptions={setOptions} runChanges={runChanges}/> <div className="PhonoChangeApplier PhonoChangeApplier--latl">
<Output results={results} setResults={setResults}/> <Latl latl={latl} dispatch={dispatch}/>
</div> <LatlOutput results={results} options={options} parseResults={parseResults} errors={errors} dispatch={dispatch}/>
</div>
</Route>
<Route exact path="/">
<Link to="/latl">LATL</Link>
<div className="PhonoChangeApplier PhonoChangeApplier--gui" data-testid="PhonoChangeApplier">
<ProtoLang lexicon={lexicon} dispatch={dispatch}/>
<Features phones={phones} features={features} dispatch={dispatch}/>
<Epochs epochs={epochs} errors={errors} dispatch={dispatch} />
<Options options={options} dispatch={dispatch}/>
<Output results={results} options={options} dispatch={dispatch}/>
</div>
</Route>
</>
); );
} }

View file

@ -0,0 +1,67 @@
@import '../public/stylesheets/variables';
div.App {
max-height: 100vh;
max-width: 100vw;
line-height: 1.25em;
padding: 1em;
a {
color: map-get($colors, 'text-input')
}
h1 {
font-size: 2em;
padding: 1em 0;
}
h3 {
font-size: 1.25em;
padding: 0.5em 0;
}
h5 {
font-size: 1.1em;
padding: 0.1em 0;
font-weight: 800;
}
div.PhonoChangeApplier--gui {
display: grid;
width: 100%;
place-items: center center;
grid-template-columns: repeat(auto-fit, minmax(25em, 1fr));
grid-template-rows: repeat(auto-fill, minmax(300px, 1fr));
div {
max-width: 100%;
max-height: 50vh;
margin: 1em;
overflow-y: scroll;
}
}
div.PhonoChangeApplier--latl {
display: flex;
flex-flow: row wrap;
}
button.form, input[type="submit"].form, input[type="button"].form {
height: 2em;
border-radius: 0.25em;
border-color: transparent;
margin: 0.2em auto;
width: 10em;
}
button.form--add, input[type="submit"].form--add, input[type="button"].form--add{
background-color: greenyellow;
color: black;
}
button.form--remove, input[type="submit"].form--remove, input[type="button"].form--remove {
background-color: red;
color: white;
}
}

View file

@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { HashRouter as Router } from 'react-router-dom';
import App from './App'; import App from './App';
import PhonoChangeApplier from './PhonoChangeApplier'; import PhonoChangeApplier from './PhonoChangeApplier';
import renderer from 'react-test-renderer'; import renderer from 'react-test-renderer';
@ -9,13 +10,13 @@ import extendExpect from '@testing-library/jest-dom/extend-expect'
it('renders PhonoChangeApplier without crashing', () => { it('renders PhonoChangeApplier without crashing', () => {
const div = document.createElement('div'); const div = document.createElement('div');
ReactDOM.render(<PhonoChangeApplier />, div); ReactDOM.render(<Router><PhonoChangeApplier /></Router>, div);
ReactDOM.unmountComponentAtNode(div); ReactDOM.unmountComponentAtNode(div);
}); });
describe('App', () => { describe('App', () => {
it('renders Proto Language Lexicon', () => { it('renders Proto Language Lexicon', () => {
const { getByTestId } = render(<PhonoChangeApplier />); const { getByTestId } = render(<Router><PhonoChangeApplier /></Router>);
expect(getByTestId('PhonoChangeApplier')).toHaveTextContent('Proto Language Lexicon'); expect(getByTestId('PhonoChangeApplier')).toHaveTextContent('Proto Language Lexicon');
}) })
}) })

View file

@ -2,44 +2,77 @@ import React from 'react';
import './Epochs.scss'; import './Epochs.scss';
import SoundChangeSuite from './SoundChangeSuite'; import SoundChangeSuite from './SoundChangeSuite';
import { render } from 'react-dom';
const Epochs = props => { const Epochs = ({epochs, errors, dispatch}) => {
const addEpoch = e => {
const addEpoch = (e, props) => {
e.preventDefault() e.preventDefault()
let index = props.epochs.length + 1; let index = epochs.length + 1;
props.setEpochs([...props.epochs, {name: `epoch ${index}`, changes:['[+ feature]>[- feature]/_#']}]) dispatch({
type: 'ADD_EPOCH',
value: {name: `epoch ${index}`}
})
} }
const removeEpoch = (e, epochName) => { const removeEpoch = (e, epochName) => {
e.preventDefault() e.preventDefault()
let newEpochs = props.epochs.filter(epoch => epoch.name !== epochName); dispatch({
props.setEpochs(newEpochs) type: 'REMOVE_EPOCH',
value: {name: epochName}
});
} }
const updateEpoch = (epoch, epochIndex) => { const updateEpoch = (epoch, epochIndex) => {
let updatedEpochs = [...props.epochs] const dispatchValue = {
updatedEpochs[epochIndex] = epoch name: epoch.name,
props.setEpochs(updatedEpochs) index: epochIndex,
changes: epoch.changes,
parent: epoch.parent
}
dispatch({
type: "SET_EPOCH",
value: dispatchValue
})
}
const renderAddEpochButton = index => {
if (epochs && index === epochs.length - 1 ) return (
<form onSubmit={e=>addEpoch(e)}>
<input className="form form--add" type="submit" name="add-epoch" value="Add Epoch" ></input>
</form>
)
return <></>
}
const renderEpochs = () => {
if (epochs && epochs.length) {
return epochs.map((epoch, index) => {
const epochError = errors.epoch ? errors.error : null
return (
<div
className="SoundChangeSuite"
data-testid={`${epoch.name}_SoundChangeSuite`}
key={`epoch-${index}`}
>
<SoundChangeSuite
epochIndex={index} epoch={epoch}
updateEpoch={updateEpoch} removeEpoch={removeEpoch}
epochs={epochs}
error={epochError}
/>
{renderAddEpochButton(index)}
</div>
)});
}
return renderAddEpochButton(-1)
} }
return ( return (
<div className="Epochs" data-testid="Epochs"> <>
<h3>Sound Change Epochs</h3> { renderEpochs() }
{props.epochs </>
? props.epochs.map((epoch, idx) => {
return <SoundChangeSuite
key={`epochname-${idx}`} epochIndex={idx} epoch={epoch}
updateEpoch={updateEpoch} removeEpoch={removeEpoch}
error={props.errors[epoch.name]}
/>})
: <></>}
<form onSubmit={e=>addEpoch(e, props)}>
<input type="submit" name="add-epoch" value="Add Epoch" ></input>
</form>
</div>
); );
} }

View file

@ -13,10 +13,6 @@ it('renders Epochs without crashing', () => {
}); });
describe('Epochs', () => { describe('Epochs', () => {
it('renders the correct subtitle', () => {
const { getByTestId } = render(<Epochs />);
expect(getByTestId('Epochs')).toHaveTextContent('Sound Change Epochs');
});
it('renders a suite of soundchanges', () => { it('renders a suite of soundchanges', () => {
const { getByTestId } = render(<Epochs />); const { getByTestId } = render(<Epochs />);

View file

@ -1,57 +1,137 @@
// @flow
import React, {useState} from 'react'; import React, {useState} from 'react';
import './Features.scss'; import './Features.scss';
const parseFeaturesFromPhonemeObject = phonemeObject => { import type { featureAction } from '../reducers/reducer.features';
let featureMap = Object.keys(phonemeObject).reduce((featureObject, phonemeName) => {
let phoneme = phonemeObject[phonemeName];
phoneme.forEach(feature => {
featureObject[feature] ? featureObject[feature].push(phonemeName) : featureObject[feature] = [ phonemeName ]
});
return featureObject;
},{})
return Object.keys(featureMap).map(feature => <li key={`feature__${feature}`}>{`[+ ${feature}] = `}{featureMap[feature].join('|')}</li>);
}
const getPhonemesFromFeatureSubmission = (props, newPhonemes, feature) => {
let newPhonemeObject = newPhonemes.split('/').reduce((phonemeObject, newPhoneme) => {
newPhoneme = newPhoneme.trim();
phonemeObject = phonemeObject[newPhoneme]
? {...phonemeObject, [newPhoneme]: [...phonemeObject[newPhoneme], feature]}
: {...phonemeObject, [newPhoneme]: [feature]}
return phonemeObject;
}, {...props.phonemes})
return newPhonemeObject;
}
const Features = (props) => { const Features = ({ phones, features, dispatch }) => {
const [feature, setFeature] = useState('nasal') const [feature, setFeature] = useState('aspirated')
const [newPhonemes, setNewPhonemes] = useState('n / m / ŋ') const [ newPositivePhones, setNewPositivePhones ] = useState('tʰ / pʰ / kʰ');
const [ newNegativePhones, setNewNegativePhones ] = useState('t / p / k');
const newFeaturesSubmit = e => { const newFeaturesSubmit = e => {
e.preventDefault(); e.preventDefault();
let newPhonemeObject = getPhonemesFromFeatureSubmission(props, newPhonemes, feature);
props.setPhonemes(newPhonemeObject);
if (!props.features || !props.features.includes(feature)) props.setFeatures([...props.features, feature])
setFeature(''); setFeature('');
setNewPhonemes(''); setNewPositivePhones('');
setNewNegativePhones('');
} }
const handleDeleteClick = (e, feature) => {
e.preventDefault();
const deleteFeatureAction = {
type: "DELETE_FEATURE",
value: feature
}
return dispatch(deleteFeatureAction);
}
const parsePhonesFromFeatureObject = featureObject => {
const getProperty = property => object => object[property]
const getFeatureMap = (featureObject) => {
return Object.keys(featureObject).map(feature => {
const plusPhones = featureObject[feature].positive.map(getProperty('grapheme')).join(' / ');
const minusPhones = featureObject[feature].negative.map(getProperty('grapheme')).join(' / ');
return {[feature]: {plus: plusPhones, minus: minusPhones}}
})
}
const getFeatureMapJSX = (featureMap) => {
return featureMap.map((feature, index) => {
const [featureName] = Object.keys(feature);
const { plus, minus } = feature[featureName];
return (
<li key={`feature__${featureName}`}>
<span className="feature--names-and-phones">
<span className="feature--feature-name">
{`[+ ${featureName} ]`}
</span>
<span className="feature--feature-phones">
{plus}
</span>
</span>
<span className="feature--names-and-phones">
<span className="feature--feature-name">
{`[- ${featureName} ]`}
</span>
<span className="feature--feature-phones">
{minus}
</span>
</span>
<button className="delete-feature" onClick={e => handleDeleteClick(e, featureName)}>X</button>
</li>
)
})
}
const featureMap = getFeatureMap(featureObject);
const featureMapJSX = getFeatureMapJSX(featureMap);
return featureMapJSX;
}
const parseNewPhones = somePhones => {
if (somePhones === '') return [''];
return somePhones.split('/').map(phone => phone.trim());
}
const handleClickDispatch = e => dispatchFunction => actionBuilder => actionParameters => {
e.preventDefault();
return dispatchFunction(actionBuilder(actionParameters));
}
const buildAddFeatureAction = ([newPositivePhones, newNegativePhones, feature]): featureAction => (
{
type: "ADD_FEATURE",
value: {
positivePhones: parseNewPhones(newPositivePhones),
negativePhones: parseNewPhones(newNegativePhones),
feature
}
}
)
return ( return (
<div className="Features" data-testid="Features"> <div className="Features" data-testid="Features">
<h3>Phonetic Features</h3> <h3>Phonetic Features</h3>
<ul className="Features__list" data-testid="Features-list"> <ul className="Features__list" data-testid="Features-list">
{props.phonemes ? <>{parseFeaturesFromPhonemeObject(props.phonemes)}</> : <></>} {phones ? <>{parsePhonesFromFeatureObject(features)}</> : <></>}
</ul> </ul>
<form className="Features__form" data-testid="Features-form"> <form className="Features__form" data-testid="Features-form">
<input <input
type="text" name="feature" type="text" name="feature"
value={feature} onChange={e=> setFeature(e.target.value)} value={feature} onChange={e=> setFeature(e.target.value)}
></input>
{/* ! Positive Phones */}
<label htmlFor="positive-phones">+
<input
id="positive-phones"
type="text" name="phonemes"
value={newPositivePhones} onChange={e=> setNewPositivePhones(e.target.value)}
></input>
</label>
{/* ! Negative Phones */}
<label htmlFor="negative-phones">-
<input
id="negative-phones"
type="text" name="phonemes"
value={newNegativePhones} onChange={e=> setNewNegativePhones(e.target.value)}
></input>
</label>
<input
className="form form--add"
type="submit"
onClick={e => handleClickDispatch(e)(dispatch)(buildAddFeatureAction)([newPositivePhones, newNegativePhones, feature])}
value="Add feature"
></input> ></input>
<input type="text" name="phonemes" value={newPhonemes} onChange={e=> setNewPhonemes(e.target.value)}></input>
<input type="submit" onClick={newFeaturesSubmit} value="Add feature"></input>
</form> </form>
</div> </div>
); );
} }

View file

@ -0,0 +1,42 @@
div.Features {
ul.Features__list {
width: 100%;
li {
display: grid;
gap: 0.5em;
grid-template-columns: 10fr 10fr 1fr;
margin: 0.5em 0;
place-items: center center;
span.feature--names-and-phones {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
place-items: center center;
}
span.feature-name {
font-weight: 600;
}
}
}
form {
display: flex;
flex-flow: column;
input {
margin: 0.1em;
font-size: 1em;
}
}
button.delete-feature {
background-color: red;
border-color: transparent;
border-radius: 0.5em;
color: white;
max-height: 1.5em;
}
}

View file

@ -19,22 +19,4 @@ describe('Features', () => {
expect(getByTestId('Features')).toHaveTextContent('Phonetic Features'); expect(getByTestId('Features')).toHaveTextContent('Phonetic Features');
}); });
it('renders features from phonemes hook', () => {
const { getByTestId } = render(<Features phonemes={ {n:[ 'nasal', 'occlusive' ]} }/>);
expect(getByTestId('Features-list')).toContainHTML('<li>[+ nasal] = n</li><li>[+ occlusive] = n</li>');
});
// it('adds new features and new phonemes from features and newPhonemes hooks', () => {
// const { getByTestId } = render(<Features />);
// getByTestId('Features-form')
// })
// it('adds features from form to hooks', () => {
// const phonemes = [];
// const setPhonemes = jest.fn()
// const { getByTestId } = render(<Features phonemes={phonemes} setPhonemes={setPhonemes}/>);
// // mock function for adding feature to state ([+ nasal] = n)
// expect(getByTestId('Features-list')).toContainHTML('<li>[+ nasal] = n</li>');
// })
}); });

28
src/components/Latl.js Normal file
View file

@ -0,0 +1,28 @@
import React from 'react';
import './Latl.scss';
const Latl = ({latl, dispatch}) => {
const { innerWidth, innerHeight } = window;
const handleChange = e => {
const setLatlAction = {
type: 'SET_LATL',
value: e.target.value
}
dispatch(setLatlAction);
}
return (
<div className="Latl">
<h3>.LATL</h3>
<textarea name="latl" id="latl"
value={latl}
cols={'' + Math.floor(innerWidth / 15)}
rows={'' + Math.floor(innerHeight / 30)}
onChange={handleChange}
/>
</div>
);
}
export default Latl;

3
src/components/Latl.scss Normal file
View file

@ -0,0 +1,3 @@
div.Latl {
min-width: 80vw;
}

View file

@ -0,0 +1,69 @@
import React from 'react';
import './LatlOutput.scss';
import Output from './Output';
const LatlOutput = ({results, options, dispatch, errors, parseResults}) => {
const handleClick = e => dispatchFunc => {
e.preventDefault()
return dispatchFunc();
}
const dispatchClear = () => {
const clearAction = {
type: 'CLEAR',
value: {}
}
dispatch(clearAction)
}
const dispatchParse = () => {
const parseAction = {
type: 'PARSE_LATL',
value: {}
}
dispatch(parseAction)
}
const dispatchRun = () => {
const runAction = {
type: 'RUN',
value: {}
}
dispatch(runAction)
}
return (
<div className="LatlOutput">
<h3>Output</h3>
<form>
<input
className="form form--remove"
type="submit"
onClick={e=>handleClick(e)(dispatchClear)}
value="Clear"
/>
<input
id="Parse"
name="Parse"
className="form form--add"
type="submit"
onClick={e=>handleClick(e)(dispatchParse)}
value="Parse"
/>
<input
id="Run"
name="Run"
className="form form--add"
type="submit"
onClick={e=>handleClick(e)(dispatchRun)}
value="Run"
/>
</form>
<Output results={results} errors={errors} options={options} parseResults={parseResults}/>
</div>
);
}
export default LatlOutput;

View file

@ -0,0 +1,9 @@
div.LatlOutput {
display: flex;
flex-flow: column nowrap;
form {
display: grid;
grid-template-columns: repeat(auto-fit, min-max(10em, 1fr));
}
}

View file

@ -2,36 +2,54 @@ import React, { useState } from 'react';
import './Options.scss'; import './Options.scss';
import ls from 'local-storage'; import ls from 'local-storage';
const Options = props => { const Options = ({ options, dispatch }) => {
const [ load, setLoad ] = useState(''); const [ load, setLoad ] = useState('');
const handleRadioChange = e => { const handleRadioChange = e => {
props.setOptions({...props.options, [e.target.name]: e.target.id}) const { name, id } = e.target;
dispatch({
type: 'SET_OPTIONS',
value: {
option: name,
setValue: id
}
});
}
const handleFormSubmit = (e, options) => {
e.preventDefault();
dispatch({
type: 'RUN',
value: options
});
} }
const handleCheckChange = e => { const handleOutputClearSubmit = e => {
props.setOptions({...props.options, [e.target.name]: e.target.checked}) e.preventDefault();
console.log('clearing')
dispatch({
type: 'CLEAR',
value: {}
});
} }
return ( return (
<div className="Options" data-testid="Options"> <div className="Options" data-testid="Options">
<h3>Modeling Options</h3> <h3>Modeling Options</h3>
<form onSubmit={e=>props.runChanges(e)} data-testid="Options-form"> <form onSubmit={e=>handleFormSubmit(e, options)} data-testid="Options-form">
{/* <h5>Output</h5> */}
<input <input
type="radio" name="output" id="default" type="radio" name="output" id="default"
checked={props.options ? props.options.output === 'default' : true} checked={options ? options.output === 'default' : true}
onChange={e=>handleRadioChange(e)} onChange={e=>handleRadioChange(e)}
/> />
<label htmlFor="default">Default <label htmlFor="default">Default
<span className="Options__output-example"> output</span> <span className="Options__output-example"> output</span>
</label> </label>
<input {/* <input
type="radio" name="output" id="proto" type="radio" name="output" id="proto"
checked={props.options ? props.options.output === 'proto' : false} checked={options ? options.output === 'proto' : false}
onChange={e=>handleRadioChange(e)} onChange={e=>handleRadioChange(e)}
/> />
<label htmlFor="proto">Proto <label htmlFor="proto">Proto
@ -40,25 +58,19 @@ const Options = props => {
<input <input
type="radio" name="output" id="diachronic" type="radio" name="output" id="diachronic"
checked={props.options ? props.options.output === 'diachronic' : false} checked={options ? options.output === 'diachronic' : false}
onChange={e=>handleRadioChange(e)} onChange={e=>handleRadioChange(e)}
/> />
<label htmlFor="diachronic">Diachronic <label htmlFor="diachronic">Diachronic
<span className="Options__output-example"> *proto > *epoch > output</span> <span className="Options__output-example"> *proto > *epoch > output</span>
</label> </label> */}
<input <input className="form form--add" type="submit" value="Run Changes"></input>
type="checkbox" name="save" <input className="form form--remove" type="button" value="Clear Output" onClick={e=>handleOutputClearSubmit(e)}/>
checked={props.options ? props.options.save : false}
onChange={e=>handleCheckChange(e)}
/>
<label htmlFor="save">Store session on Run</label>
<input type="submit" value="Run Changes"></input>
</form> </form>
<form onSubmit={() => {}}> {/* <form onSubmit={()=>{}}>
<label> <label>
Load from a prior run: Load from a prior run:
<select value={load} onChange={e=>setLoad(e.target.value)}> <select value={load} onChange={e=>setLoad(e.target.value)}>
@ -70,7 +82,7 @@ const Options = props => {
</select> </select>
</label> </label>
<input type="submit" value="Submit" /> <input type="submit" value="Submit" />
</form> </form> */}
</div> </div>
); );
} }

View file

@ -0,0 +1,9 @@
div.Options {
form {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.5em;
}
}

View file

@ -19,9 +19,4 @@ describe('Options', () => {
expect(getByTestId('Options')).toHaveTextContent('Modeling Options'); expect(getByTestId('Options')).toHaveTextContent('Modeling Options');
}); });
it('renders form options from props', () => {
let options = {output: 'proto', save: true}
const { getByTestId } = render(<Options options={options} />)
expect(getByTestId('Options-form')).toHaveFormValues(options);
})
}); });

View file

@ -2,12 +2,34 @@ import React from 'react';
import './Output.scss'; import './Output.scss';
const Output = props => { const Output = props => {
const { results, options, errors, parseResults } = props;
const renderResults = () => {
switch(options.output) {
case 'default':
return renderDefault();
default:
return <></>
}
}
const renderDefault = () => {
return results.map((epoch, i) => {
const lexicon = epoch.lexicon.map((lexeme, i) => <span key={`${epoch.pass}-${i}`}>{lexeme}</span>);
return (
<div key={`epoch-${i}`} className="Output-epoch">
<h5>{epoch.pass}</h5>
<p className="lexicon">{lexicon}</p>
</div>
)
})
}
return ( return (
<div className="Output" data-testid="Output"> <div className="Output" data-testid="Output">
<h3>Results of Run</h3> <h3>Results of Run</h3>
<div data-testid="Output-lexicon" className="Output__container">
<div data-testid="Output-lexicon"> {parseResults ? parseResults : <></>}
{props.results ? props.results.map((lexicalItem, i) => <p key={`output-lexical-item-${i}`}>{lexicalItem}</p>) : <></>} {results && results.length ? renderResults() : <></>}
</div> </div>
</div> </div>
); );

View file

@ -0,0 +1,18 @@
div.Output {
div.Output__container {
display: flex;
flex-flow: row wrap;
}
div.Output-epoch {
display: flex;
flex-flow: column;
p.lexicon {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(5em, 1fr));
}
}
}

View file

@ -20,8 +20,10 @@ describe('Output', () => {
}); });
it('renders output lexicon list from output hook', () => { it('renders output lexicon list from output hook', () => {
const { getByTestId } = render(<Output results={['word', 'lex', 'word']}/>); const { getByTestId } = render(<Output results={[{pass: 'test', lexicon: ['word', 'lex', 'word']}]} options={{output: 'default'}}/>);
expect(getByTestId('Output-lexicon')).toContainHTML('<p>word</p><p>lex</p><p>word</p>'); expect(getByTestId('Output-lexicon')).toContainHTML(wordListWordHTML);
}); });
}); });
const wordListWordHTML = '<div class="Output-epoch"><h5>test</h5><p class="lexicon"><span>word</span><span>lex</span><span>word</span></p></div>';

View file

@ -1,7 +1,26 @@
import React from 'react'; import React from 'react';
import './ProtoLang.scss'; import './ProtoLang.scss';
const ProtoLang = (props) => { const ProtoLang = ({ lexicon, dispatch }) => {
const getProperty = property => object => object[property];
const renderLexicon = () => {
if (!lexicon) return '';
// Code for optionally rendering epoch name with lexeme
// `\t#${lexeme.epoch.name}`
return lexicon.map(getProperty('lexeme')).join('\n');
}
const handleChange = e => {
const value = e.target.value.split(/\n/).map(line => {
const lexeme = line.split('#')[0].trim();
const epoch = line.split('#')[1] || '';
return { lexeme, epoch }
})
dispatch({
type: 'SET_LEXICON',
value
})
}
return ( return (
<div className="ProtoLang" data-testid="ProtoLang"> <div className="ProtoLang" data-testid="ProtoLang">
<h3>Proto Language Lexicon</h3> <h3>Proto Language Lexicon</h3>
@ -9,8 +28,11 @@ const ProtoLang = (props) => {
<form data-testid="ProtoLang-Lexicon"> <form data-testid="ProtoLang-Lexicon">
<textarea <textarea
name="lexicon" name="lexicon"
value={props.lexicon ? props.lexicon.join("\n") : ''} cols="30"
onChange={e=>props.setLexicon(e.target.value.split(/\n/))} rows="10"
data-testid="ProtoLang-Lexicon__textarea"
value={renderLexicon()}
onChange={e => handleChange(e)}
> >
</textarea> </textarea>
</form> </form>

View file

@ -1,5 +0,0 @@
.ProtoLang {
width: 100%;
height: 100%;
color: black;
}

View file

@ -3,7 +3,7 @@ import ReactDOM from 'react-dom';
import ProtoLang from './ProtoLang'; import ProtoLang from './ProtoLang';
import renderer from 'react-test-renderer'; import renderer from 'react-test-renderer';
import { exportAllDeclaration } from '@babel/types'; import { exportAllDeclaration } from '@babel/types';
import {render} from '@testing-library/react'; import {render, fireEvent} from '@testing-library/react';
import extendExpect from '@testing-library/jest-dom/extend-expect' import extendExpect from '@testing-library/jest-dom/extend-expect'
it('renders ProtoLang without crashing', () => { it('renders ProtoLang without crashing', () => {
@ -13,15 +13,15 @@ it('renders ProtoLang without crashing', () => {
}); });
describe('ProtoLang', () => { describe('ProtoLang', () => {
it('renders the correct subtitle', () => { it('renders the correct subtitle', () => {
const { getByTestId } = render(<ProtoLang />); const { getByTestId } = render(<ProtoLang />);
expect(getByTestId('ProtoLang')).toHaveTextContent('Proto Language Lexicon'); expect(getByTestId('ProtoLang')).toHaveTextContent('Proto Language Lexicon');
}) });
it('renders lexicon from state', () => { it('renders lexicon from state', () => {
const { getByTestId } = render(<ProtoLang lexicon={['one']}/>); const { getByTestId } = render(<ProtoLang lexicon={[{ lexeme:'one', epoch:{name: 'epoch-one', changes: []} }]}/>);
expect(getByTestId('ProtoLang-Lexicon')).toHaveFormValues({lexicon: 'one'}); expect(getByTestId('ProtoLang-Lexicon')).toHaveFormValues({lexicon: 'one'});
}) });
}) })

View file

@ -1,31 +1,91 @@
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import './SoundChangeSuite.scss'; import './SoundChangeSuite.scss';
const SoundChangeSuite = props => { const SoundChangeSuite = props => {
const [ epoch, setEpoch ] = useState(props.epoch ? props.epoch : {name:'', changes:['']}); const { epochIndex, error, removeEpoch, epochs } = props;
const [ epoch, setEpoch ] = useState(props.epoch ? props.epoch : {name:'', changes:[''], parent:'none'});
const changeHandler = (e,cb) => { const changeHandler = (e,cb) => {
cb(e); cb(e);
props.updateEpoch(epoch, props.epochIndex); props.updateEpoch(epoch, epochIndex);
}
useEffect(() => {
props.updateEpoch(epoch, epochIndex);
}, [epoch])
const renderOptionFromEpoch = thisEpoch => (
<option
key={`${epoch.name}__parent-option--${thisEpoch.name}`}
value={thisEpoch.name}
>
{thisEpoch.name}
</option>
)
const replaceCurrentEpoch = thisEpoch => {
if (thisEpoch.name === epoch.name) return {name: 'none'}
return thisEpoch;
}
const isViableParent = thisEpoch => {
if (thisEpoch.parent && thisEpoch.parent === epoch.name) return false;
return true;
}
const parentsOptions = () => {
return epochs.map(replaceCurrentEpoch).filter(isViableParent).map(renderOptionFromEpoch)
}
const renderParentInput = () => {
if (epochIndex) return (
<>
<label htmlFor={`${epoch.name}-parent`}>
Parent Epoch:
</label>
<select
name="parent"
list={`${epoch.name}-parents-list`}
value={epoch.parent || 'none'}
onChange={e=>changeHandler(
e, ()=>setEpoch({...epoch, parent:e.target.value})
)
}
>
{parentsOptions()}
</select>
</>
)
return <></>
}
const renderError = () => {
if (error) return (
<p className="error">{error}</p>
)
return <></>
} }
return ( return (
<div className="SoundChangeSuite" data-testid={`${epoch.name}_SoundChangeSuite`}> <>
<h4>{epoch.name}</h4> <h4>{epoch.name}</h4>
{renderError()}
<form className="SoundChangeSuite__form" data-testid={`${epoch.name}_SoundChangeSuite_changes`}> <form className="SoundChangeSuite__form" data-testid={`${epoch.name}_SoundChangeSuite_changes`}>
<label htmlFor={`${epoch.name}-name`}>
<textarea Name:
</label>
<input type="text"
name="epoch" name="epoch"
id="" cols="30" rows="1" id={`${epoch.name}-name`} cols="30" rows="1"
value={epoch.name} value={epoch.name}
onChange={e=>changeHandler( onChange={e=>changeHandler(
e, () => setEpoch({...epoch, name:e.target.value}) e, () => {
setEpoch({...epoch, name:e.target.value})
}
)} )}
></textarea> ></input>
{renderParentInput()}
{props.error
? <p><span className="error-message">{`Formatting errors in line(s) ${props.error.join(', ')}`}</span></p>
: <></>}
<textarea <textarea
name="changes" name="changes"
id="" cols="30" rows="10" id="" cols="30" rows="10"
@ -40,10 +100,10 @@ const SoundChangeSuite = props => {
)} )}
></textarea> ></textarea>
</form> </form>
<form onSubmit={e=>props.removeEpoch(e, epoch.name)}> <form onSubmit={e=>removeEpoch(e, epoch.name)}>
<input type="submit" name="remove-epoch" value={`remove ${epoch.name}`}></input> <input className="form form--remove" type="submit" name="remove-epoch" value={`remove ${epoch.name}`}></input>
</form> </form>
</div> </>
); );
} }

View file

@ -8,18 +8,18 @@ import extendExpect from '@testing-library/jest-dom/extend-expect'
it('renders SoundChangeSuite without crashing', () => { it('renders SoundChangeSuite without crashing', () => {
const div = document.createElement('div'); const div = document.createElement('div');
ReactDOM.render(<SoundChangeSuite />, div); ReactDOM.render(<SoundChangeSuite epoch={{name:'Epoch Name', changes:['sound change rule']}} updateEpoch={()=>{}} removeEpoch={()=>{}}/>, div);
ReactDOM.unmountComponentAtNode(div); ReactDOM.unmountComponentAtNode(div);
}); });
describe('SoundChangeSuite', () => { describe('SoundChangeSuite', () => {
it('renders the correct subtitle', () => {
const { getByTestId } = render(<SoundChangeSuite epoch={{name:'Epoch Name', changes:['sound change rule']}}/>);
expect(getByTestId('Epoch Name_SoundChangeSuite')).toHaveTextContent('Epoch Name');
});
it('renders a suite of soundchanges', () => { it('renders a suite of soundchanges', () => {
const { getByTestId } = render(<SoundChangeSuite epoch={{name:'Epoch Name', changes:['sound>change/environment']}}/>); const { getByTestId } = render(
<SoundChangeSuite epoch={{name:'Epoch Name', changes:['sound>change/environment']}}
updateEpoch={()=>{}} removeEpoch={()=>{}}
/>
);
expect(getByTestId('Epoch Name_SoundChangeSuite_changes')).toHaveFormValues({changes: 'sound>change/environment'}) expect(getByTestId('Epoch Name_SoundChangeSuite_changes')).toHaveFormValues({changes: 'sound>change/environment'})
}) })
}); });

View file

@ -1,13 +0,0 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
}

View file

@ -1,10 +1,12 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import './index.css'; import { HashRouter as Router } from 'react-router-dom';
import './index.scss';
import App from './App'; import App from './App';
import * as serviceWorker from './serviceWorker'; import * as serviceWorker from './serviceWorker';
ReactDOM.render(<App />, document.getElementById('root')); ReactDOM.render(<Router><App /></Router>
, document.getElementById('root'));
// If you want your app to work offline and load faster, you can change // If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls. // unregister() to register() below. Note this comes with some pitfalls.

24
src/index.scss Normal file
View file

@ -0,0 +1,24 @@
@import '../public/stylesheets/variables';
body {
margin: 0;
font-family: 'Catamaran', Arial, Helvetica, sans-serif;
background-color: map-get($colors, 'main--bg');
color: map-get($colors, 'main');
textarea, input[type="text"] {
background-color: map-get($colors, 'text-input--bg');
color: map-get($colors, 'text-input');
border: 1px solid map-get($colors, 'main');
font-family: 'Fira Code', monospace;
}
code {
font-family: 'Fira Code', monospace;
}
p.error {
color: map-get($colors, 'error');
}
}

View file

@ -0,0 +1,3 @@
export const clearOutput = (state, action) => {
return { ...state, results: [], errors: {}, parseResults: '' };
}

View file

@ -0,0 +1,41 @@
// @flow
import type { stateType } from './reducer';
export type epochAction = {
type: "ADD_EPOCH" | "SET_EPOCH" | "REMOVE_EPOCH",
value: {
index?: number,
name: string,
changes?: Array<string>,
parent?: string
}
}
export const addEpoch = (state: stateType, action: epochAction): stateType => {
const newEpoch = { name: action.value.name, changes: action.value.changes || [''], parent: null};
return {...state, epochs: [...state.epochs, newEpoch]}
}
export const setEpoch = (state: stateType, action: epochAction): stateType => {
const index = action.value.index;
if (typeof index !== 'number') return state;
const mutatedEpochs = state.epochs;
mutatedEpochs[index].name = action.value.name
? action.value.name
: mutatedEpochs[index].name;
mutatedEpochs[index].changes = action.value.changes
? action.value.changes
: mutatedEpochs[index].changes;
mutatedEpochs[index].parent = action.value.parent && action.value.parent !== 'none'
? action.value.parent
: null
return {...state, epochs: [...mutatedEpochs]}
}
export const removeEpoch = (state: stateType, action: epochAction): stateType => {
const mutatedEpochs = state.epochs.filter(epoch => epoch.name !== action.value.name )
return {...state, epochs: [...mutatedEpochs]}
}

View file

@ -1,12 +1,13 @@
import {stateReducer} from './stateReducer'; import {stateReducer} from './reducer';
describe('Epochs', () => { describe('Epochs', () => {
const state = {}; const state = {};
beforeEach(()=> { beforeEach(()=> {
state.epochs = [ state.epochs = [
{ {
name: 'epoch 1', name: 'epoch-1',
changes: [''] changes: [''],
parent: null
} }
] ]
}) })
@ -17,36 +18,46 @@ describe('Epochs', () => {
}); });
it('epochs addition returns new epochs list', () => { it('epochs addition returns new epochs list', () => {
const action = {type: 'ADD_EPOCH', value: { name: 'epoch 2', changes: ['']}}; const action = {type: 'ADD_EPOCH', value: { name: 'epoch-2', changes: [''], parent: null}};
expect(stateReducer(state, action)).toEqual({...state, epochs: [...state.epochs, action.value]}) expect(stateReducer(state, action)).toEqual({...state, epochs: [...state.epochs, action.value]})
}) })
it('epoch name mutation returns new epochs list with mutation', () => { it('epoch-name mutation returns new epochs list with mutation', () => {
const firstAction = {type: 'ADD_EPOCH', value: { name: 'epoch 2', changes: ['']}}; const firstAction = {type: 'ADD_EPOCH', value: { name: 'epoch-2', changes: ['']}};
const secondAction = {type: 'SET_EPOCH', value: { index: 0, name: 'proto-lang'}}; const secondAction = {type: 'SET_EPOCH', value: { index: 0, name: 'proto-lang'}};
const secondState = stateReducer(state, firstAction); const secondState = stateReducer(state, firstAction);
expect(stateReducer(secondState, secondAction)).toEqual( expect(stateReducer(secondState, secondAction)).toEqual(
{...state, {...state,
epochs: [ epochs: [
{name: 'proto-lang', changes: ['']}, {name: 'proto-lang', changes: [''], parent: null},
{name: 'epoch 2', changes: ['']} {name: 'epoch-2', changes: [''], parent: null}
] ]
} }
); );
}); });
it('epoch changes mutation returns new epochs list with mutation', () => { it('epoch changes mutation returns new epochs list with mutation', () => {
const firstAction = {type: 'ADD_EPOCH', value: { name: 'epoch 2', changes: ['']}}; const firstAction = {type: 'ADD_EPOCH', value: { name: 'epoch-2', changes: ['']}};
const secondAction = {type: 'SET_EPOCH', value: { index: 0, changes: ['n>t/_#', '[+plosive]>[+nasal -plosive]/_n']}}; const secondAction = {type: 'SET_EPOCH', value: { index: 0, changes: ['n>t/_#', '[+plosive]>[+nasal -plosive]/_n']}};
const secondState = stateReducer(state, firstAction); const secondState = stateReducer(state, firstAction);
expect(stateReducer(secondState, secondAction)).toEqual( expect(stateReducer(secondState, secondAction)).toEqual(
{...state, {...state,
epochs: [ epochs: [
{name: 'epoch 1', changes: ['n>t/_#', '[+plosive]>[+nasal -plosive]/_n']}, {name: 'epoch-1', changes: ['n>t/_#', '[+plosive]>[+nasal -plosive]/_n'], parent: null},
{name: 'epoch 2', changes: ['']} {name: 'epoch-2', changes: [''], parent: null}
] ]
} }
); );
});
it('epochs returned with deleted epoch removed', () => {
const firstAction = {type: 'ADD_EPOCH', value: { name: 'epoch-2', changes: ['']}};
const stateWithTwoEpochs = stateReducer(state, firstAction);
const secondAction = {type: 'REMOVE_EPOCH', value: {index: 0, name: 'epoch-1'}}
expect(stateReducer(stateWithTwoEpochs, secondAction)).toEqual({
...state,
epochs: [{ name: 'epoch-2', changes: [''], parent: null}]
});
}); });
}); });

View file

@ -1,5 +1,5 @@
// @flow // @flow
import type { stateType } from './stateReducer'; import type { stateType } from './reducer';
export type featureAction = { export type featureAction = {
type: "ADD_FEATURE", type: "ADD_FEATURE",
@ -12,43 +12,55 @@ export type featureAction = {
const addPhones = (phones: {}, phone: string): {} => { const addPhones = (phones: {}, phone: string): {} => {
let node = {}; let node = {};
phone.split('').forEach((graph, index) => { phone.split('').forEach((graph, index) => {
if (index) node[graph] = {} if (index) node[graph] = {}
if (!index && !phones[graph]) phones[graph] = {} if (!index && !phones[graph]) phones[graph] = {}
node = index === 0 ? phones[graph] : node[graph]; node = index === 0 ? phones[graph] : node[graph];
if (index === phone.length - 1) node.grapheme = phone; if (index === phone.length - 1) node.grapheme = phone;
}) })
return phones; return phones;
} }
const findPhone = (phones: {}, phone: string): {} => { const findPhone = (phones: {}, phone: string): {} => {
let node = {}; return phone
phone.split('').forEach((graph, index) => { .split('')
node = index === 0 ? phones[graph] : node[graph]; .reduce((node, graph, index) => {
}); node = index === 0 ? phones[graph] : node[graph];
return node; return node;
}, {});
} }
const addFeatureToPhone = ( const addFeatureToPhone = (
phones: {}, phone: string, featureKey: string, featureValue: boolean phones: {}, phone: string, featureKey: string, featureValue: boolean
): {} => ): {} => {
{ try {
let node = {} let node = {}
phone.split('').forEach((graph, index) => { phone.split('').forEach((graph, index) => {
node = index === 0 ? phones[graph] : node[graph]; node = index === 0 ? phones[graph] : node[graph];
if (index === phone.split('').length - 1) node.features = {...node.features, [featureKey]: featureValue}
}) if (index === phone.split('').length - 1) {
node.features = node && node.features
? {...node.features, [featureKey]: featureValue }
: {[featureKey]: featureValue};
}
});
return phones; return phones;
}
catch (e) {
throw { phones, phone, featureKey, featureValue }
}
} }
export const addFeature = (state: stateType, action: featureAction): stateType => { export const addFeature = (state: stateType, action: featureAction): stateType => {
let positivePhones = action.value.positivePhones || []; let positivePhones = action.value.positivePhones || [];
let negativePhones = action.value.negativePhones || []; let negativePhones = action.value.negativePhones || [];
let newFeatureName = action.value.feature; let newFeatureName = action.value.feature;
let newPhoneObject = [ let newPhoneObject = [
...positivePhones, ...negativePhones ...positivePhones, ...negativePhones
].reduce((phoneObject, phone) => addPhones(phoneObject, phone), state.phones) ]
.reduce((phoneObject, phone) => addPhones(phoneObject, phone), state.phones)
if (positivePhones) { if (positivePhones) {
@ -72,4 +84,12 @@ export const addFeature = (state: stateType, action: featureAction): stateType =
let newFeature = {[action.value.feature]: {positive: positivePhones, negative: negativePhones}}; let newFeature = {[action.value.feature]: {positive: positivePhones, negative: negativePhones}};
return {...state, features:{...state.features, ...newFeature}, phones: newPhoneObject} return {...state, features:{...state.features, ...newFeature}, phones: newPhoneObject}
}
export const deleteFeature = (state, action) => {
const deletedFeature = state.features[action.value];
deletedFeature.positive.forEach(phone => delete phone.features[action.value])
deletedFeature.negative.forEach(phone => delete phone.features[action.value])
delete state.features[action.value];
return state
} }

View file

@ -1,4 +1,4 @@
import {stateReducer} from './stateReducer'; import {stateReducer} from './reducer';
describe('Features', () => { describe('Features', () => {
const state = {} const state = {}
@ -31,4 +31,17 @@ describe('Features', () => {
); );
}); });
it('feature deletion returns new feature list', () => {
const action = {type: 'DELETE_FEATURE', value: 'occlusive'}
expect(stateReducer(state, action)).toEqual(
{...state,
features: {},
phones: {
a: {features: {}, grapheme: 'a'},
n: {features: {}, grapheme: 'n'}
}
}
)
})
}); });

View file

@ -0,0 +1,739 @@
// @flow
import type { stateType } from './reducer';
export type initAction = {
type: "INIT"
}
export const clearState = () => {
return {
epochs: [],
phones: {},
options: { output: 'default', save: false },
results: [],
errors: {},
features: {},
lexicon: [],
latl: '',
parseResults: ''
}
}
export const waffleState = () => {
return {
epochs: [],
phones: {},
options: { output: 'default', save: false },
results: [],
errors: {},
features: {},
lexicon: [],
latl: waffleLatl,
parseResults: ''
}
}
export const initState = (changesArgument: number): stateType => {
const state = {
epochs: [
{
name: 'epoch-1',
changes: [
'[+ occlusive - nasal]>[+ occlusive + nasal]/n_.',
'a>ɯ/._#',
'[+ sonorant - low rounded high back]>0/._.',
'[+ obstruent]>[+ obstruent aspirated ]/#_.',
'[+ sonorant - rounded]>[+ sonorant + rounded]/._#',
// 'at>ta/._#'
]
}
],
phones: {
a: {
grapheme: 'a', features: {
sonorant: true, back: true, low: true, high: false, rounded: false
}
},
u: {
grapheme: 'u', features: {
sonorant: true, back: true, low: false, high: true, rounded: true,
}
},
ɯ: {
grapheme: 'ɯ', features: {
sonorant: true, back: true, low: false, high: true, rounded: false,
}
},
ə: {
grapheme: 'ə', features: {
sonorant: true, low: false, rounded: false, high: false, back: false
}
},
t: {
grapheme: 't', features: {
occlusive: true, coronal: true, obstruent: true, nasal: false
},
ʰ: {
grapheme: 'tʰ', features: {
occlusive: true, coronal: true, obstruent: true, aspirated: true
}
}
},
n: {
grapheme: 'n', features: {
sonorant: true, nasal: true, occlusive: true, coronal: true
}
}
},
options: {
output: 'default', save: false
},
results: [],
errors: {},
features: {},
lexicon: [],
latl: '',
parseResults: ''
};
state.features = {
sonorant: { positive:[ state.phones.a, state.phones.u, state.phones.ɯ, state.phones.ə, state.phones.n], negative: [] },
back: { positive:[ state.phones.a, state.phones.u, state.phones.ɯ ], negative: [ state.phones.ə ] },
low: { positive:[ state.phones.a ], negative: [ state.phones.u, state.phones.ɯ, state.phones.ə ] },
high: { positive:[ state.phones.u, state.phones.ɯ ], negative: [ state.phones.a, state.phones.ə ] },
rounded: { positive:[ state.phones.u ], negative: [ state.phones.a, state.phones.ɯ, state.phones.ə ] },
occlusive: { positive:[ state.phones.t, state.phones.n, state.phones.t.ʰ ], negative: [] },
coronal: { positive:[ state.phones.t, state.phones.n, state.phones.t.ʰ ], negative: [] },
obstruent: { positive:[ state.phones.t, state.phones.n, state.phones.t.ʰ ], negative: [] },
nasal: { positive:[ state.phones.n ], negative: [state.phones.t, state.phones.t.ʰ] },
aspirated: { positive:[ state.phones.t.ʰ ], negative: [ state.phones.t ] },
}
state.lexicon = [
{lexeme: 'anta', epoch: state.epochs[0]},
{lexeme: 'anat', epoch: state.epochs[0]},
{lexeme: 'anət', epoch: state.epochs[0]},
{lexeme: 'anna', epoch: state.epochs[0]},
{lexeme: 'tan', epoch: state.epochs[0]},
{lexeme: 'ənta', epoch: state.epochs[0]}
]
if(changesArgument > -1) state.epochs[0].changes = state.epochs[0].changes.splice(0, changesArgument)
return state;
}
const waffleLatl = `
; -------- main class features
[consonantal
+=
; PLOSIVES
p / pʼ / / t / tʼ / ɾ / k / kʼ / /
; AFFRICATES
/ /
; FRICATIVES
f / v / θ / ð / s / z / ʃ / ʒ / ç / x /
; NASALS
m ɱ / n / ŋ /
; LIQUIDS + RHOTICS
l / ɹ ɹʲ ɹˤ /
; SYLLABIC CONSONANTS
m̩ / n̩ / l̩ / ɹ̩
-=
; VOWELS
æ / e / ə / ɑ / ɔ / ɪ̞ / ɛ / ʌ / ʊ̞ / i / u̟ /
; GLIDES
j / w /
; LARYNGEALS
h ɦ / ʔ
]
[sonorant
+=
; VOWELS
æ / e / ə / ɑ / ɔ / ɪ̞ / ɛ / ʌ / ʊ̞ / i / u̟ /
; GLIDES
j / w w̥ /
; LIQUIDS + RHOTICS
l / ɹ ɹʲ ɹˤ /
; NASALS
m ɱ / n / ŋ /
; SYLLABIC CONSONANTS
m̩ / n̩ / l̩ / ɹ̩
-=
; PLOSIVES
p / pʼ / / t / tʼ / ɾ / k / kʼ / /
; AFFRICATES
/ /
; FRICATIVES
f / v / θ / ð / s / z / ʃ / ʒ / ç / x /
; LARYNGEALS
h ɦ / ʔ
]
[approximant
+=
; VOWELS
æ / e / ə / ɑ / ɔ / ɪ̞ / ɛ / ʌ / ʊ̞ / i / u̟ /
; LIQUIDS + RHOTICS
l / ɹ ɹʲ ɹˤ /
; GLIDES
j / w /
; SYLLABIC LIQUIDS
l̩ / ɹ̩
-=
; PLOSIVES
p / pʼ / / t / tʼ / ɾ / k / kʼ / /
; AFFRICATES
/ /
; FRICATIVES
f / v / θ / ð / s / z / ʃ / ʒ / ç / x /
; NASALS
m ɱ / n / ŋ /
; SYLLABIC NASALS
m̩ / n̩
]
; -------- laryngeal features
[voice
+=
; VOWELS
æ / e / ə / ɑ / ɔ / ɪ̞ / ɛ / ʌ / ʊ̞ / i / u̟ /
; GLIDES
j / w /
; LIQUIDS + RHOTICS
l / ɹ ɹʲ ɹˤ /
; NASALS
m ɱ / n / ŋ /
; SYLLABIC CONSONANTS
m̩ / n̩ / l̩ / ɹ̩ /
; VOICED FRICATIVES
v / ð / z / ʒ /
; VOICED AFFRICATES
/
; VOICED LARYNGEALS
; LARYNGEALS
ɦ
-= voiceless obstruents
; PLOSIVES
p / pʼ / / t / tʼ / ɾ / k / kʼ / /
; VOICELESS AFFRICATES
/ /
; VOICELESS FRICATIVES
f / θ / s / ʃ / ç / x /
; VOICELESS LARYNGEALS
h / ʔ
]
[spreadGlottis
+=
; ASPIRATED PLOSIVES
/ / /
; ASPIRATED AFFRICATES
/
; SPREAD LARYNGEALS
h ɦ
-=
; VOWELS
æ / e / ə / ɑ / ɔ / ɪ̞ / ɛ / ʌ / ʊ̞ / i / u̟ /
; UNASPIRATED PLOSIVES
p / pʼ / t / tʼ / ɾ / k / kʼ /
; UNASPIRATED AFFRICATES
/ /
; FRICATIVES
f / v / θ / ð / s / z / ʃ / ʒ / ç / x /
; NASAL OBSTRUENTS
m ɱ / n / ŋ /
; LIQUIDS + RHOTICS
l / ɹ ɹʲ ɹˤ /
; SYLLABIC CONSONANTS
m̩ / n̩ / l̩ / ɹ̩ /
; GLIDES
j / w
; CONSTRICTED LARYNGEALS
ʔ
]
[constrictedGlottis
+=
; LARYNGEALIZED RHOTIC
ɹˤ /
; CONSTRICTED LARYNGEAL
ʔ /
; EJECTIVE PLOSIVES
pʼ / tʼ / kʼ
-=
; VOWELS
æ / e / ə / ɑ / ɔ / ɪ̞ / ɛ / ʌ / ʊ̞ / i / u̟ /
; PLOSIVES
p / / t / ɾ / k / /
; AFFRICATES
/ /
; FRICATIVES
f / v / θ / ð / s / z / ʃ / ʒ / ç / x /
; NASAL OBSTRUENTS
m ɱ / n / ŋ /
; LIQUIDS
l /
; NON-PHARYNGEALIZED RHOTICS
ɹ ɹʲ /
; SYLLABIC CONSONANTS
m̩ / n̩ / l̩ / ɹ̩
; GLIDES
j / w
; SPREAD LARYNGEALS
h ɦ /
]
; -------- manner features
[continuant
+=
; FRICATIVES
f / v / θ / ð / s / z / ʃ / ʒ / ç / x /
; VOWELS
æ / e / ə / ɑ / ɔ / ɪ̞ / ɛ / ʌ / ʊ̞ / i / u̟ /
; LIQUIDS + RHOTICS
l / ɹ ɹʲ ɹˤ /
; GLIDES
j / w /
; SYLLABIC LIQUIDS
l̩ / ɹ̩ /
; TAPS
ɾ
-=
; NON-TAP PLOSIVES
p / pʼ / / t / tʼ / / k / kʼ / /
; AFFRICATES
/ /
; NASALS
m ɱ / n / ŋ /
; SYLLABIC NASALS
m̩ / n̩
]
[nasal
+=
; NASALS
m ɱ / n / ŋ /
; SYLLABIC NASALS
m̩ / n̩
-=
; VOWELS
æ / e / ə / ɑ / ɔ / ɪ̞ / ɛ / ʌ / ʊ̞ / i / u̟ /
; FRICATIVES
f / v / θ / ð / s / z / ʃ / ʒ / ç / x /
; LIQUIDS + RHOTICS
l / ɹ ɹʲ ɹˤ /
; GLIDES
j / w /
; SYLLABIC LIQUIDS
l̩ / ɹ̩ /
; PLOSIVES
p / pʼ / / t / tʼ / ɾ / k / kʼ / /
; AFFRICATES
/ /
]
[strident
+=
; STRIDENT FRICATIVES
f / v / s / z / ʃ / ʒ /
; STRIDENT AFFRICATES
/
-=
; VOWELS
æ / e / ə / ɑ / ɔ / ɪ̞ / ɛ / ʌ / ʊ̞ / i / u̟ /
; PLOSIVES
p / pʼ / / t / tʼ / ɾ / k / kʼ / /
; NON-STRIDENT FRICATIVES
θ / ð / ç / x /
; NASAL OBSTRUENTS
m ɱ / n / ŋ /
; RHOTICS + LIQUIDS
l / ɹ ɹʲ ɹˤ /
; SYLLABIC CONSONANTS
m̩ / n̩ / l̩ / ɹ̩ /
; GLIDES
j / w
]
[lateral
+=
; LATERAL LIQUIDS
l /
; SYLLABIC LATERALS /
l̩
-=
; VOWELS
æ / e / ə / ɑ / ɔ / ɪ̞ / ɛ / ʌ / ʊ̞ / i / u̟ /
; PLOSIVES
p / pʼ / / t / tʼ / ɾ / k / kʼ /
; AFFRICATES
/
; FRICATIVES
f / v / θ / ð / s / z / ʃ / ʒ / ç / x
; NASAL OBSTRUENTS
m ɱ / n / ŋ
; RHOTIC LIQUIDS
ɹ ɹʲ ɹˤ
; NON-LIQUID SYLLABIC CONSONANTS
m̩ / n̩ / ɹ̩
; GLIDES
j / w
]
; -------- ---- PLACE features
; -------- labial features
[labial
+=
; ROUNDED VOWELS
u̟ / ʊ̞ / ɔ /
; LABIAL PLOSIVES
p / pʼ / /
; LABIAL FRICATIVES
f / v /
; LABIAL NASALS
m ɱ /
; LABIAL SYLLABIC CONSONANTS
m̩ /
; LABIAL GLIDES
w
-=
; UNROUNDED VOWELS
æ / e / ə / ɑ / ɪ̞ / ɛ / ʌ / i /
; NON-LABIAL PLOSIVES
t / tʼ / ɾ / k / kʼ / /
; NON-LABIAL AFFRICATES
/ /
; NON-LABIAL FRICATIVES
θ / ð / s / z / ʃ / ʒ / ç / x /
; NON-LABIAL NASAL OBSTRUENTS
n / ŋ /
; LIQUIDS
l /
; RHOTIC LIQUIDS
ɹ ɹʲ ɹˤ /
; NON-LABIAL SYLLABIC CONSONANTS
n̩ / l̩ / ɹ̩ /
; NON-LABIAL GLIDES
j
]
; -------- coronal features
[coronal
+=
; CORONAL PLOSIVES
t / tʼ / ɾ /
; CORONAL AFFRICATES
/ /
; CORONAL FRICATIVES
θ / ð / s / z / ʃ / ʒ /
; CORONAL NASALS
n /
; CORONAL LIQUIDS
l
; CORONAL RHOTIC LIQUIDS
ɹ
; CORONAL SYLLABIC CONSONANTS
n̩ / l̩ / ɹ̩
-=
; VOWELS
æ / e / ə / ɑ / ɔ / ɪ̞ / ɛ / ʌ / ʊ̞ / i / u̟ /
; NON-CORONAL PLOSIVES
p / pʼ / / k / kʼ /
; NON-CORONAL FRICATIVES
f / v / ç / x
; NON-CORONAL NASAL OBSTRUENTS
m ɱ / ŋ
; NON-CORONAL RHOTIC LIQUIDS
ɹʲ ɹˤ
; NON-CORONAL SYLLABIC CONSONANTS
m̩ /
; NON-CORONAL GLIDES
j / w
]
[anterior
+=
; ALVEOLAR PLOSIVES
t / tʼ / ɾ /
; ALVEOLAR AFFRICATES
/ /
; DENTAL FRICATIVES
θ / ð /
; ALVEOLAR FRICATIVES
s / z /
; ALVEOLAR NASALS
n /
; ALVEOLAR LIQUIDS
l
; ALVEOLAR SYLLABIC CONSONANTS
n̩ / l̩ /
-=
; POSTALVEOLAR FRICATIVES
ʃ / ʒ /
; POSTALVEOLAR RHOTIC LIQUIDS
ɹ /
; POSTALVEOLAR SYLLABIC CONSONANTS
ɹ̩ /
; -- NON-CORONALs
; VOWELS
æ / e / ə / ɑ / ɔ / ɪ̞ / ɛ / ʌ / ʊ̞ / i / u̟ /
; NON-CORONAL PLOSIVES
p / pʼ / / k / kʼ /
; NON-CORONAL FRICATIVES
f / v / ç / x
; NON-CORONAL NASAL OBSTRUENTS
m ɱ / ŋ
; NON-CORONAL RHOTIC LIQUIDS
ɹʲ ɹˤ
; NON-CORONAL SYLLABIC CONSONANTS
m̩ /
; NON-CORONAL GLIDES
j / w
]
[distributed
+=
; DENTAL FRICATIVES
θ / ð /
; POSTALVEOLAR FRICATIVES
ʃ / ʒ /
; POSTALVEOLAR RHOTIC LIQUIDS
ɹ /
; POSTALVEOLAR SYLLABIC CONSONANTS
ɹ̩ /
-=
; apical / retroflex
; ALVEOLAR PLOSIVES
t / tʼ / ɾ /
; ALVEOLAR FRICATIVES
s / z /
; ALVEOLAR NASALS
n /
; ALVEOLAR LIQUIDS
l
; ALVEOLAR SYLLABIC CONSONANTS
n̩ / l̩ /
; -- NON-CORONALS
; VOWELS
æ / e / ə / ɑ / ɔ / ɪ̞ / ɛ / ʌ / ʊ̞ / i / u̟ /
; NON-CORONAL PLOSIVES
p / pʼ / / k / kʼ /
; NON-CORONAL FRICATIVES
f / v / ç / x
; NON-CORONAL NASAL OBSTRUENTS
m ɱ / ŋ
; NON-CORONAL RHOTIC LIQUIDS
ɹʲ ɹˤ
; NON-CORONAL SYLLABIC CONSONANTS
m̩ /
; NON-CORONAL GLIDES
j / w
]
; -------- dorsal features
[dorsal
+=
; VOWELS
æ / e / ə / ɑ / ɔ / ɪ̞ / ɛ / ʌ / ʊ̞ / i / u̟ /
; DORSAL PLOSIVES
k / kʼ / /
; DORSAL FRICATIVES
ç / x /
; DORSAL NASAL OBSTRUENTS
ŋ /
; DORSAL RHOTIC LIQUIDS
ɹʲ ɹˤ
; DORSAL GLIDES
j
-=
; NON-DORSAL PLOSIVES
p / pʼ / / t / tʼ / ɾ /
; NON-DORSAL AFFRICATES
/ /
; NON-DORSAL FRICATIVES
f / v / θ / ð / s / z / ʃ / ʒ /
; NON-DORSAL NASALS
m ɱ / n /
; NON-DORSAL LIQUIDS
l
; NON-DORSAL RHOTIC LIQUIDS
ɹ
; NON-DORSAL SYLLABIC CONSONANTS
m̩ / n̩ / l̩ / ɹ̩
; NON-DORSAL GLIDES
w
]
[high
+=
; HIGH VOWELS
i / u̟ / ʊ̞ / ɪ̞
; HIGH DORSAL PLOSIVES
k / kʼ / /
; HIGH DORSAL FRICATIVES
ç / x /
; HIGH DORSAL NASAL OBSTRUENTS
ŋ /
; HIGH RHOTIC LIQUIDS
ɹʲ
; HIGH DORSAL GLIDES
j / w
-= χ / e / o / a
; NON-HIGH VOWELS
ɑ / æ / e / ə / ɛ / ʌ
; NON-HIGH RHOTIC LIQUIDS
ɹˤ
; -- NON-DORSALS
; NON-DORSAL PLOSIVES
p / pʼ / / t / tʼ / ɾ /
; NON-DORSAL AFFRICATES
/ /
; NON-DORSAL FRICATIVES
f / v / θ / ð / s / z / ʃ / ʒ /
; NON-DORSAL NASALS
m ɱ / n /
; NON-DORSAL LIQUIDS
l
; NON-DORSAL RHOTIC LIQUIDS
ɹ
; NON-DORSAL SYLLABIC CONSONANTS
m̩ / n̩ / l̩ / ɹ̩
; NON-DORSAL GLIDES
w
]
[low
+=
; LOW VOWELS
ɑ / æ / ɛ /
; LOW DORSAL RHOTIC LIQUIDS
ɹˤ
-= a / ɛ / ɔ
; NON-LOW VOWELS
i / u̟ / ʊ̞ / ɪ̞ / e / ə / ʌ
; NON-LOW DORSAL PLOSIVES
k / kʼ / /
; NON-LOW DORSAL FRICATIVES
ç / x /
; NON-LOW DORSAL NASAL OBSTRUENTS
ŋ /
; NON-LOW DORSAL RHOTIC LIQUIDS
ɹʲ
; DORSAL GLIDES
j
; -- NON-DORSALS
; NON-DORSAL PLOSIVES
p / pʼ / / t / tʼ / ɾ /
; NON-DORSAL AFFRICATES
/ /
; NON-DORSAL FRICATIVES
f / v / θ / ð / s / z / ʃ / ʒ /
; NON-DORSAL NASALS
m ɱ / n /
; NON-DORSAL LIQUIDS
l
; NON-DORSAL RHOTIC LIQUIDS
ɹ
; NON-DORSAL SYLLABIC CONSONANTS
m̩ / n̩ / l̩ / ɹ̩
; NON-DORSAL GLIDES
w
]
[back
+=
; BACK VOWELS
ɑ / ɔ / ʌ / ʊ̞ / u̟ /
; BACK DORSAL PLOSIVES
k / kʼ / /
; BACK DORSAL FRICATIVES
x /
; BACK DORSAL NASAL OBSTRUENTS
ŋ /
; BACK DORSAL RHOTIC LIQUIDS
ɹˤ
-=
; NON-BACK DORSAL FRICATIVES
ç /
; NON-BACK DORSAL RHOTIC LIQUIDS
ɹʲ
; NON-BACK DORSAL GLIDES
j
; NON-BACK VOWELS
æ / e / ə / ɪ̞ / ɛ / i
; -- NON-DORSALS
; NON-DORSAL PLOSIVES
p / pʼ / / t / tʼ / ɾ /
; NON-DORSAL AFFRICATES
/ /
; NON-DORSAL FRICATIVES
f / v / θ / ð / s / z / ʃ / ʒ /
; NON-DORSAL NASALS
m ɱ / n /
; NON-DORSAL LIQUIDS
l
; NON-DORSAL RHOTIC LIQUIDS
ɹ
; NON-DORSAL SYLLABIC CONSONANTS
m̩ / n̩ / l̩ / ɹ̩
; NON-DORSAL GLIDES
w
]
[tense ; compare to ATR or RTR
+=
; TENSE VOWELS
e / i / u̟ / ɑ
-=
; NON-TENSE VOWELS
æ / ə / ɪ̞ / ɛ / ʌ / ʊ̞ / ɔ /
; DORSAL PLOSIVES
k / kʼ / /
; DORSAL FRICATIVES
ç / x /
; DORSAL NASAL OBSTRUENTS
ŋ /
; DORSAL RHOTIC LIQUIDS
ɹʲ ɹˤ /
; DORSAL GLIDES
j
; -- NON-DORSALS
; NON-DORSAL PLOSIVES
p / pʼ / / t / tʼ / ɾ /
; NON-DORSAL AFFRICATES
/ /
; NON-DORSAL FRICATIVES
f / v / θ / ð / s / z / ʃ / ʒ /
; NON-DORSAL NASALS
m ɱ / n /
; NON-DORSAL LIQUIDS
l
; NON-DORSAL RHOTIC LIQUIDS
ɹ
; NON-DORSAL SYLLABIC CONSONANTS
m̩ / n̩ / l̩ / ɹ̩
; NON-DORSAL GLIDES
w
]
*PROTO
; -- Devoicing, all our z's become s's
[+ voice - continuant]>[- voice]/._.
; -- Reduction of schwa
ə>0/._.
|Gif Lang
*PROTO
; -- Ejectivization, all our pits become pit's
[+ spreadGlottis - continuant]>[+ constrictedGlottis - spreadGlottis]/._[+ constrictedGlottis]
[+ spreadGlottis - continuant]>[+ constrictedGlottis - spreadGlottis]/[+ constrictedGlottis]_.
[+ constrictedGlottis]>0/[+ constrictedGlottis - continuant]_.
[+ constrictedGlottis]>0/._[+ constrictedGlottis - continuant]
|Jif Lang
`

74
src/reducers/reducer.js Normal file
View file

@ -0,0 +1,74 @@
// @flow
import { addLexeme, setLexicon } from './reducer.lexicon';
import type { lexiconAction } from './reducer.lexicon';
import { addEpoch, setEpoch, removeEpoch } from './reducer.epochs';
import type { epochAction } from './reducer.epochs';
import { addFeature, deleteFeature } from './reducer.features';
import type { featureAction } from './reducer.features';
import type { optionsAction } from './reducer.options';
import { setOptions } from './reducer.options';
import { run } from './reducer.results';
import type { resultsAction } from './reducer.results'
import { initState } from './reducer.init';
import type { initAction } from './reducer.init';
import { clearOutput } from './reducer.clear';
import { setLatl, parseLatl } from './reducer.latl';
export type stateType = {
lexicon: Array<{lexeme: string, epoch: epochType}>,
epochs: Array<epochType>,
phones: {[key: string]: phoneType},
options: {output: string, save: boolean},
results: [],
errors: {},
features: featureType
}
type epochType = {
name: string, changes: Array<string>
}
type phoneType = {
grapheme: string,
features: {[key: string]: boolean}
}
type featureType = {
[key: string]: {[key: string]: Array<phoneType>}
}
type actionType = featureAction | epochAction | initAction | resultsAction | lexiconAction
export const stateReducer = (state: stateType, action: actionType): stateType => {
switch (action.type) {
case 'INIT': {
return initState();
}
case 'ADD_LEXEME': return addLexeme(state, action);
case 'SET_LEXICON': return setLexicon(state, action);
case 'ADD_EPOCH': return addEpoch(state, action);
case 'SET_EPOCH': return setEpoch(state, action);
case 'REMOVE_EPOCH': return removeEpoch(state, action);
case 'ADD_FEATURE': return addFeature(state, action);
case 'DELETE_FEATURE': return deleteFeature(state, action);
case 'SET_OPTIONS': return setOptions(state, action);
case 'SET_LATL': return setLatl(state, action);
case 'PARSE_LATL': return parseLatl(state, action);
case 'CLEAR': return clearOutput(state, action);
case 'RUN': return run(state, action);
default: return state;
}
}

View file

@ -0,0 +1,529 @@
import { stateReducer } from './reducer';
export const setLatl = (state, action) => {
let latl = action.value;
return {...state, latl, parseResults: ''};
}
const getOneToken = (latl, tokens) => {
for (const [type, regEx] of tokenTypes) {
const newRegEx = new RegExp(`^(${regEx})`);
const match = latl.match(newRegEx) || null;
if (match) {
const newTokens = [...tokens, {type, value: match[0].trim()}]
const newLatl = latl.slice(match[0].length ,);
return [newLatl, newTokens]
}
}
throw `Unexpected token at ${latl.split('\n')[0]}`
}
export const tokenize = latl => {
let i = 0;
let tokens = [];
let newLatl = latl.trim();
try {
while(newLatl.length) {
[newLatl, tokens] = getOneToken(newLatl, tokens)
}
return tokens;
}
catch (err) {
return {errors: 'tokenization error', message: err, newLatl}
}
}
const parseLineBreak = (tree, token, index, tokens) => {
const lastNode = tree[tree.length - 1];
if (!lastNode) return tree;
switch (lastNode.type) {
case 'rule': {
if (tree[tree.length - 2].type === 'ruleSet') {
const ruleValue = lastNode.value;
tree[tree.length - 2].value.push(ruleValue);
tree.pop()
return tree;
}
if (tree[tree.length - 2].type === 'epoch') {
const newNode = { type: 'ruleSet', value: [ lastNode.value ] }
tree[tree.length - 1] = newNode;
return tree;
}
}
case 'feature--plus': {
// tree[tree.length - 1].type === 'feature';
return tree;
}
case 'feature--minus': {
// tree[tree.length - 1].type === 'feature';
return tree;
}
default:
return tree;
}
}
const parseWhiteSpace = (tree, token, index, tokens) => {
const lastNode = tree[tree.length - 1];
switch (lastNode.type) {
case 'rule': {
tree[tree.length - 1] = {...lastNode, value: lastNode.value + ' ' }
return tree;
}
default:
return tree;
}
}
const parseStar = (tree, token, index, tokens) => {
const nextToken = tokens[index + 1];
if (nextToken.type === 'referent') {
return [...tree, { type: 'epoch-parent' }]
}
}
const parsePipe = (tree, token, index, tokens) => {
const nextToken = tokens[index + 1];
if (nextToken.type === 'referent') {
const ruleToken = tree[tree.length - 1];
const epochToken = tree[tree.length - 2];
if (ruleToken.type === 'rule' || ruleToken.type === 'ruleSet') {
if (epochToken.type === 'epoch') {
tree[tree.length - 2] = {
...epochToken,
changes: [...ruleToken.value],
type: 'epoch-name'
}
tree.pop();
return tree;
}
}
}
return [...tree, 'unexpected pipe']
}
const parseReferent = (tree, token, index, tokens) => {
const lastNode = tree[tree.length - 1];
switch (lastNode.type) {
case 'epoch-parent': {
tree[tree.length - 1] = {...lastNode, parent: token.value, type: 'epoch' }
return tree;
}
case 'epoch-name': {
tree[tree.length - 1] = {...lastNode, name: token.value, type: 'epoch' }
return [...tree, { type: 'main'}];
}
case 'epoch': {
return [...tree, { type: 'rule', value: token.value } ]
}
case 'rule': {
tree[tree.length - 1] = {...lastNode, value: lastNode.value + token.value }
return tree;
}
case 'ruleSet': {
return [...tree, { type: 'rule', value: token.value }]
}
case 'feature': {
if (!lastNode.value) {
tree[tree.length - 1].value = token.value;
return tree;
}
}
case 'feature--plus': {
if (lastNode.value) {
lastNode.positivePhones = [...lastNode.positivePhones, token.value ]
}
else {
lastNode.value = token.value;
}
tree[tree.length - 1] = lastNode;
return [...tree]
}
case 'feature--minus': {
if (lastNode.value) {
lastNode.negativePhones = [...lastNode.negativePhones, token.value ]
}
else {
lastNode.value = token.value;
}
tree[tree.length - 1] = lastNode;
return [...tree]
}
case 'lexicon': {
if (!lastNode.epoch) {
tree[tree.length - 1].epoch = token.value;
}
else {
tree[tree.length - 1].value.push(token.value)
}
return tree;
}
default:
return [...tree, `unexpected referent ${token.value}`]
}
}
const parsePhone = (tree, token, index, tokens) => {
const lastNode = tree[tree.length - 1];
switch(lastNode.type) {
case 'rule': {
tree[tree.length - 1] = {...lastNode, value: lastNode.value + token.value }
return tree;
}
case 'ruleSet': {
return [...tree, { type: 'rule', value: token.value }]
}
case 'feature--plus':
lastNode.positivePhones = [...lastNode.positivePhones, token.value ];
tree[tree.length - 1] = lastNode;
return tree;
case 'feature--minus':
lastNode.negativePhones = [...lastNode.negativePhones, token.value ];
tree[tree.length - 1] = lastNode;
return tree;
default:
return [...tree, `unexpected phone ${token.value}`]
}
}
const parseOpenBracket = (tree, token, index, tokens) => {
const lastNode = tree[tree.length - 1];
if (lastNode) {
switch (lastNode.type) {
case 'epoch':
return [...tree, {type: 'rule', value: token.value}]
case 'rule':
tree[tree.length - 1] = {...lastNode, value: lastNode.value + token.value }
return tree;
case 'ruleSet':
return [...tree, {type: 'rule', value: token.value}];
// case 'feature':
// return [{type: 'feature', positivePhones: [], negativePhones: []}];
case 'feature--plus':
return [...tree, {type: 'feature', positivePhones: [], negativePhones: []}];
case 'feature--minus':
return [...tree, {type: 'feature', positivePhones: [], negativePhones: []}];
case 'main':
return [...tree, {type: 'feature', positivePhones: [], negativePhones: []}];
default:
return [...tree, 'unexpected open bracket']
}
}
return [{type: 'feature', positivePhones: [], negativePhones: []}]
}
const parseCloseBracket = (tree, token, index, tokens) => {
const lastNode = tree[tree.length - 1];
switch (lastNode.type) {
case 'rule':
tree[tree.length - 1] = {...lastNode, value: lastNode.value + token.value }
return tree;
case 'feature--plus':
return tree;
case 'feature--minus':
return tree;
default:
return [...tree, 'unexpected close bracket']
}
}
const parsePositiveAssignment = (tree, token, index, tokens) => {
const lastNode = tree[tree.length - 1];
switch (lastNode.type) {
case 'feature':
tree[tree.length - 1].type = 'feature--plus'
return tree;
default:
return [...tree, 'unexpected positive assignment']
}
}
const parseNegativeAssignment = (tree, token, index, tokens) => {
const lastNode = tree[tree.length - 1];
switch (lastNode.type) {
case 'feature':
tree[tree.length - 1].type = 'feature--minus'
return tree;
case 'feature--plus':
tree[tree.length - 1].type = 'feature--minus';
return tree;
default:
return [...tree, 'unexpected negative assignment']
}
}
const parsePlus = (tree, token, index, tokens) => {
const lastNode = tree[tree.length - 1];
switch (lastNode.type) {
case 'rule':
tree[tree.length - 1] = {...lastNode, value: lastNode.value + token.value}
return tree;
case 'feature':
tree[tree.length - 1] = {...lastNode, type: 'feature--plus'}
return tree;
case 'feature--minus':
tree[tree.length - 1] = {...lastNode, type: 'feature--minus'}
return tree;
default:
return [...tree, 'unexpected plus']
}
}
const parseMinus = (tree, token, index, tokens) => {
const lastNode = tree[tree.length - 1];
switch (lastNode.type) {
case 'rule':
tree[tree.length - 1] = {...lastNode, value: lastNode.value + token.value}
return tree;
case 'feature':
tree[tree.length - 1] = {...lastNode, type: 'feature--minus'}
return tree;
default:
return [...tree, 'unexpected minus']
}
}
const parseEqual = (tree, token, index, tokens) => {
const lastNode = tree[tree.length - 1];
switch (lastNode.type) {
case 'feature--plus':
return tree;
case 'feature--minus':
return tree;
default:
return [...tree, 'unexpected equal'];
}
}
const parseGreaterThan = (tree, token, index, tokens) => {
const lastNode = tree[tree.length - 1];
switch (lastNode.type) {
case 'rule':
tree[tree.length - 1] = {...lastNode, value: lastNode.value + token.value}
return tree;
default:
return [...tree, 'unexpected greater than']
}
}
const parseSlash = (tree, token, index, tokens) => {
const lastNode = tree[tree.length - 1];
if (lastNode) {
switch (lastNode.type) {
case 'rule':
tree[tree.length - 1] = {...lastNode, value: lastNode.value + token.value}
return tree;
case 'feature--plus':
return tree;
case 'feature--minus':
return tree;
case 'lexicon':
return [...tree, { }];
case 'main':
return [...tree, { type: 'lexicon', value: []}]
default:
return [...tree, 'unexpected slash']
}
}
return [...tree, { type: 'lexicon', value: []}]
}
const parseHash = (tree, token, index, tokens) => {
const lastNode = tree[tree.length - 1];
switch (lastNode.type) {
case 'rule':
tree[tree.length - 1] = {...lastNode, value: lastNode.value + token.value}
return tree;
default:
return [...tree, 'unexpected hash']
}
}
const parseDot = (tree, token, index, tokens) => {
const lastNode = tree[tree.length - 1];
switch (lastNode.type) {
case 'rule':
tree[tree.length - 1] = {...lastNode, value: lastNode.value + token.value}
return tree;
default:
return [...tree, 'unexpected dot']
}
}
const parseUnderScore = (tree, token, index, tokens) => {
const lastNode = tree[tree.length - 1];
switch (lastNode.type) {
case 'rule':
tree[tree.length - 1] = {...lastNode, value: lastNode.value + token.value}
return tree;
default:
return [...tree, 'unexpected underscore']
}
}
const generateNode = (tree, token, index, tokens) => {
switch (token.type) {
// if comment, consume without effect
case 'semicolon':
return [...tree]
case 'lineBreak':
return parseLineBreak(tree, token, index, tokens);
case 'whiteSpace':
return parseWhiteSpace(tree, token, index, tokens);
// if *PROTO consume token:* and add epochs: [ { parent: 'PROTO' } ]
case 'star':
return parseStar(tree, token, index, tokens);
case 'pipe':
return parsePipe(tree, token, index, tokens);
case 'referent':
return parseReferent(tree, token, index, tokens);
case 'phone':
return parsePhone(tree, token, index, tokens);
case 'openBracket':
return parseOpenBracket(tree, token, index, tokens);
case 'closeBracket':
return parseCloseBracket(tree, token, index, tokens);
case 'positiveAssignment':
return parsePositiveAssignment(tree, token, index, tokens);
case 'negativeAssignment':
return parseNegativeAssignment(tree, token, index, tokens);
case 'plus':
return parsePlus(tree, token, index, tokens);
case 'minus':
return parseMinus(tree, token, index, tokens);
case 'equal':
return parseEqual(tree, token, index, tokens);
case 'greaterThan':
return parseGreaterThan(tree, token, index, tokens);
case 'slash':
return parseSlash(tree, token, index, tokens);
case 'hash':
return parseHash(tree, token, index, tokens);
case 'dot':
return parseDot(tree, token, index, tokens);
case 'underscore':
return parseUnderScore(tree, token, index, tokens);
default:
return [...tree, { ...token }]
}
}
const addToken = (tree, token, index, tokens) => generateNode(tree, token, index, tokens);
const connectNodes = (tree, node, index, nodes) => {
switch (node.type) {
case 'epoch':
delete node.type;
return {...tree, epochs: [...tree.epochs, {...node, index: tree.epochs.length } ] }
case 'feature':
node.feature = node.value;
delete node.value;
delete node.type;
return {...tree, features: [...tree.features, {...node } ] }
case 'feature--minus':
node.feature = node.value;
delete node.value;
delete node.type;
if (tree.features.length && tree.features[tree.features.length - 1].feature === node.feature) {
tree.features[tree.features.length - 1].negativePhones = node.negativePhones
return tree;
}
return {...tree, features: [...tree.features, {...node} ] }
case 'feature--plus':
delete node.type;
node.feature = node.value;
delete node.value;
if (tree.features.length && tree.features[tree.features.length - 1].feature === node.feature) {
tree.features[tree.features.length - 1].positivePhones = node.positivePhones
return tree;
}
return {...tree, features: [...tree.features, {...node} ] }
case 'lexicon':
delete node.type;
return {...tree, lexicon: [...tree.lexicon, node]}
default:
return tree;
}
}
export const buildTree = tokens => {
const bareTree = {
epochs: [],
features: [],
lexicon: []
}
const nodes = tokens.reduce(addToken, []);
// return nodes
const tree = nodes.reduce(connectNodes, bareTree);
const filterProps = Object.entries(tree).filter(([key, value]) => !value.length)
.map(([key, value]) => key)
return filterProps.reduce((tree, badProp) => {
delete tree[badProp];
return tree;
}, tree);
}
export const generateAST = latl => {
// tokenize
const tokens = tokenize(latl.trim());
// build tree
const tree = buildTree(tokens);
return tree;
}
export const parseLatl = (state, action) => {
try {
const latl = state.latl;
const AST = generateAST(latl);
const features = AST.features;
if (features) {
if (state.features) {
state = Object.keys(state.features).reduce((state, feature) => {
return stateReducer(state, {type: 'DELETE_FEATURE', value: feature})
}, state)
}
state = features.reduce((state, feature) => stateReducer(state, {type:'ADD_FEATURE', value: feature}), state);
}
delete AST.features;
const lexicon = AST.lexicon;
if (lexicon) {
if (state.lexicon) {
state.lexicon = [];
}
state = lexicon.reduce((state, epoch) => {
return epoch.value.reduce((reducedState, lexeme) => {
return stateReducer(reducedState, {type: 'ADD_LEXEME', value: { lexeme, epoch: epoch.epoch }})
}, state)
}, state)
}
delete AST.lexicon;
Object.entries(AST).forEach(([key, value]) => state[key] = value);
return { ...state, parseResults: 'latl parsed successfully', results:[] }
}
catch (e) {
console.log(e)
return { ...state, parseResults: 'error parsing', errors: e}
}
}
const tokenTypes = [
['semicolon', ';.*\n'],
[`star`, `\\*`],
['pipe', `\\|`],
['openBracket', `\\[`],
['closeBracket', `\\]`],
['positiveAssignment', `\\+=`],
['negativeAssignment', `\\-=`],
['plus', `\\+`],
['minus', `\\-`],
['greaterThan', `\\>`],
['hash', `#`],
['slash', `\/`],
['dot', `\\.`],
['underscore', `\\_`],
[`referent`, `[A-Za-z]+[\u00c0-\u03FFA-Za-z0-9\\-\\_]*`],
[`phone`, `[\u00c0-\u03FFA-Za-z0]+`],
['equal', `=`],
[`lineBreak`, `\\n`],
[`whiteSpace`, `\\s+`]
]

View file

@ -0,0 +1,520 @@
import { stateReducer } from './reducer';
import { initState } from './reducer.init';
import { tokenize, buildTree, parseLatl } from './reducer.latl';
describe('LATL', () => {
it('returns state unaltered with no action body', () => {
const state = initState();
const action = {
type: 'SET_LATL',
value: ''
}
const returnedState = stateReducer(state, action)
expect(returnedState).toStrictEqual(state);
})
it('returns tokens from well-formed latl epoch definition', () => {
const tokens = tokenize(epochDefinitionLatl);
expect(tokens).toStrictEqual(tokenizedEpoch)
});
it('returns tokens from well-formed latl feature definition', () => {
const tokens = tokenize(featureDefinitionLatl);
expect(tokens).toStrictEqual(tokenizedFeature);
});
it('returns tokens from well-formed latl lexicon definition', () => {
const tokens = tokenize(lexiconDefinitionLatl);
expect(tokens).toStrictEqual(tokenizedLexicon);
});
it('returns tokens from well-formed latl epoch, feature, and lexicon definitions', () => {
const latl = epochDefinitionLatl + '\n' + featureDefinitionLatl + '\n' + lexiconDefinitionLatl;
const tokens = tokenize(latl);
const lineBreaks = [{ type: 'lineBreak', value: '' },{ type: 'lineBreak', value: '' },{ type: 'lineBreak', value: '' }]
const tokenizedLatl = [...tokenizedEpoch, ...lineBreaks, ...tokenizedFeature, ...lineBreaks, ...tokenizedLexicon];
expect(tokens).toStrictEqual(tokenizedLatl);
});
it('returns AST from well-formed epoch tokens', () => {
const tree = buildTree(tokenizedEpoch);
expect(tree).toStrictEqual(treeEpoch);
})
it('returns AST from well-formed feature tokens', () => {
const tree = buildTree(tokenizedFeature);
expect(tree).toStrictEqual(treeFeature);
})
it('returns AST from well-formed lexicon tokens', () => {
const tree = buildTree(tokenizedLexicon);
expect(tree).toStrictEqual(treeLexicon);
})
it('parse returns state from well-formed feature latl', () => {
const state = initState();
const setAction = {
type: 'SET_LATL',
value: featureDefinitionLatl
}
const latlState = stateReducer(state, setAction);
const parseState = parseLatl(latlState, {});
expect(parseState).toStrictEqual(featureState)
})
it('returns run from well-formed epoch latl', () => {
const state = initState();
const setAction = {
type: 'SET_LATL',
value: runEpochLatl
}
const latlState = stateReducer(state, setAction);
const parseState = parseLatl(latlState, {})
// expect(parseState).toStrictEqual(epochState);
parseState.lexicon[0].epoch = 'PROTO'
const runState = stateReducer(parseState, {type: 'RUN', value:{}})
expect(runState).toStrictEqual({...runState, results: runEpochResults})
})
it('returns state from well-formed lexicon latl', () => {
const state = initState();
const setAction = {
type: 'SET_LATL',
value: lexiconDefinitionLatl
}
const latlState = stateReducer(state, setAction);
const parseState = parseLatl(latlState, {});
expect(parseState).toStrictEqual(lexiconState)
})
// it('returns state from well formed latl', () => {
// const state = initState();
// const setAction = {
// type: 'SET_LATL',
// value: totalLatl
// }
// const latlState = stateReducer(state, setAction);
// const parseState = parseLatl(latlState, {});
// expect(parseState).toStrictEqual(totalLatlState)
// })
})
const epochDefinitionLatl = `
; comment
*PROTO
[+ FEATURE]>[- FEATURE]/._.
n>m/#_.
|CHILD
`
const runEpochLatl = `
; comment
*PROTO
a>u/._.
|epoch-1
`
const runEpochResults = [
{
pass: 'epoch-1',
parent: 'PROTO',
lexicon: [ 'untu', 'unut', 'unət', 'unnu', 'tun', 'əntu' ]
}
]
const tokenizedEpoch = [
{ type: "semicolon", value: "; comment" },
{ type: "star", value: "*" }, { type: "referent", value: "PROTO" }, { type: 'lineBreak', value: '' }, { type: "whiteSpace", value: "" },
{ type: "openBracket", value: "[" }, { type: "plus", value: "+" }, { type: "whiteSpace", value: "" }, { type: "referent", value: "FEATURE" }, { type: "closeBracket", value: "]" },
{ type: "greaterThan", value: ">" }, { type: "openBracket", value: "[" }, { type: "minus", value: "-" }, { type: "whiteSpace", value: "" }, { type: "referent", value: "FEATURE" }, { type: "closeBracket", value: "]" },
{ type: "slash", value: "/" }, { type: "dot", value: "." },
{ type: "underscore", value: "_" }, { type: "dot", value: "." }, { type: 'lineBreak', value: '' }, { type: "whiteSpace", value: "" },
{ type: "referent", value: "n" },
{ type: "greaterThan", value: ">" }, { type: "referent", value: "m" },
{ type: "slash", value: "/" }, { type: "hash", value: "#" },
{ type: "underscore", value: "_" }, { type: "dot", value: "." }, { type: 'lineBreak', value: '' },
{ type: "pipe", value: "|" }, { type: "referent", value: "CHILD" }
]
const treeEpoch = {
epochs: [
{
parent: 'PROTO',
name: 'CHILD',
index: 0,
changes: [
'[+ FEATURE]>[- FEATURE]/._.',
'n>m/#_.'
]
}
]
}
const epochState = {
...initState(),
epochs: treeEpoch.epochs,
latl: epochDefinitionLatl
}
const featureDefinitionLatl = `
[+ PLOSIVE] = kp/p/b/d/t/g/k
[- PLOSIVE] = m/n/s/z
[SONORANT
+= m/n
-= s/z/kp/p/b/d/t/g/k
]
`
const tokenizedFeature = [
{type: "openBracket", value: "[" }, { type: "plus", value: "+" }, { type: "whiteSpace", value: "" }, { type: "referent", value: "PLOSIVE" }, { type: "closeBracket", value: "]" }, { type: "whiteSpace", value: "" },
{ type: "equal", value: "=" }, { type: "whiteSpace", value: "" }, { type: "referent", value: "kp" }, { type: "slash", value: "/" }, { type: "referent", value: "p" }, { type: "slash", value: "/" }, { type: "referent", value: "b" }, { type: "slash", value: "/" }, { type: "referent", value: "d" }, { type: "slash", value: "/" }, { type: "referent", value: "t" }, { type: "slash", value: "/" }, { type: "referent", value: "g" }, { type: "slash", value: "/" }, { type: "referent", value: "k" }, { type: 'lineBreak', value: '' },
{type: "openBracket", value: "[" }, { type: "minus", value: "-" }, { type: "whiteSpace", value: "" }, { type: "referent", value: "PLOSIVE" }, { type: "closeBracket", value: "]" }, { type: "whiteSpace", value: "" },
{ type: "equal", value: "=" }, { type: "whiteSpace", value: "" }, { type: "referent", value: "m" }, { type: "slash", value: "/" }, { type: "referent", value: "n" }, { type: "slash", value: "/" }, { type: "referent", value: "s" }, { type: "slash", value: "/" }, { type: "referent", value: "z" }, { type: 'lineBreak', value: '' },
{type: "openBracket", value: "[" }, { type: "referent", value: "SONORANT" }, { type: 'lineBreak', value: '' },
{ type: "whiteSpace", value: "" }, { type: "positiveAssignment", value: "+=" }, { type: "whiteSpace", value: "" },
{ type: "referent", value: "m" }, { type: "slash", value: "/" }, { type: "referent", value: "n" }, { type: 'lineBreak', value: '' },
{ type: "whiteSpace", value: "" }, { type: "negativeAssignment", value: "-=" }, { type: "whiteSpace", value: "" },
{ type: "referent", value: "s" }, { type: "slash", value: "/" }, { type: "referent", value: "z" }, { type: "slash", value: "/" }, { type: "referent", value: "kp" }, { type: "slash", value: "/" }, { type: "referent", value: "p" }, { type: "slash", value: "/" }, { type: "referent", value: "b" }, { type: "slash", value: "/" }, { type: "referent", value: "d" }, { type: "slash", value: "/" }, { type: "referent", value: "t" }, { type: "slash", value: "/" }, { type: "referent", value: "g" }, { type: "slash", value: "/" }, { type: "referent", value: "k" }, { type: 'lineBreak', value: '' },
{ type: "closeBracket", value: "]" },
]
const treeFeature = { features: [
{
feature: 'PLOSIVE',
positivePhones: ['kp', 'p', 'b', 'd', 't', 'g', 'k'],
negativePhones: ['m', 'n', 's', 'z']
},
{
feature: 'SONORANT',
positivePhones: ['m', 'n'],
negativePhones: ['s' ,'z' ,'kp' ,'p' ,'b' ,'d' ,'t' ,'g' ,'k']
}
]}
const featureState = {
...initState(),
features: {
PLOSIVE: {
negative: [
{
features: {
PLOSIVE: false,
SONORANT: true,
},
grapheme: "m",
},
{
features: {
PLOSIVE: false,
SONORANT: true,
},
grapheme: "n",
},
{
features: {
PLOSIVE: false,
SONORANT: false,
},
grapheme: "s",
},
{
features: {
PLOSIVE: false,
SONORANT: false,
},
grapheme: "z",
},
],
positive: [
{
features: {
PLOSIVE: true,
},
grapheme: "kp",
},
{
features: {
PLOSIVE: true,
SONORANT: false,
},
grapheme: "p",
},
{
features: {
PLOSIVE: true,
SONORANT: false,
},
grapheme: "b",
},
{
features: {
PLOSIVE: true,
SONORANT: false,
},
grapheme: "d",
},
{
features: {
PLOSIVE: true,
SONORANT: false,
},
grapheme: "t",
ʰ: {
features: {},
grapheme: "tʰ",
},
},
{
features: {
PLOSIVE: true,
SONORANT: false,
},
grapheme: "g",
},
{
features: {
PLOSIVE: true,
SONORANT: false,
},
grapheme: "k",
p: {
features: {
SONORANT: false,
},
grapheme: "kp",
},
},
],
},
SONORANT: {
negative: [
{
features: {
PLOSIVE: false,
SONORANT: false,
},
grapheme: "s",
},
{
features: {
PLOSIVE: false,
SONORANT: false,
},
grapheme: "z",
},
{
features: {
SONORANT: false,
},
grapheme: "kp",
},
{
features: {
PLOSIVE: true,
SONORANT: false,
},
grapheme: "p",
},
{
features: {
PLOSIVE: true,
SONORANT: false,
},
grapheme: "b",
},
{
features: {
PLOSIVE: true,
SONORANT: false,
},
grapheme: "d",
},
{
features: {
PLOSIVE: true,
SONORANT: false,
},
grapheme: "t",
ʰ: {
features: {},
grapheme: "tʰ",
},
},
{
features: {
PLOSIVE: true,
SONORANT: false,
},
grapheme: "g",
},
{
features: {
PLOSIVE: true,
SONORANT: false,
},
grapheme: "k",
p: {
features: {
SONORANT: false,
},
grapheme: "kp",
},
},
],
positive: [
{
features: {
PLOSIVE: false,
SONORANT: true,
},
grapheme: "m",
},
{
features: {
PLOSIVE: false,
SONORANT: true,
},
grapheme: "n",
},
],
}, },
parseResults: 'latl parsed successfully',
latl: featureDefinitionLatl,
phones: {
a: {
features: {},
grapheme: "a",
},
b: {
features: {
PLOSIVE: true,
SONORANT: false,
},
grapheme: "b",
},
d: {
features: {
PLOSIVE: true,
SONORANT: false,
},
grapheme: "d",
},
g: {
features: {
PLOSIVE: true,
SONORANT: false,
},
grapheme: "g",
},
k: {
features: {
PLOSIVE: true,
SONORANT: false,
},
grapheme: "k",
p: {
features: {
SONORANT: false,
},
grapheme: "kp",
},
},
m: {
features: {
PLOSIVE: false,
SONORANT: true,
},
grapheme: "m",
},
n: {
features: {
PLOSIVE: false,
SONORANT: true,
},
grapheme: "n",
},
p: {
features: {
PLOSIVE: true,
SONORANT: false,
},
grapheme: "p",
},
s: {
features: {
PLOSIVE: false,
SONORANT: false,
},
grapheme: "s",
},
t: {
features: {
PLOSIVE: true,
SONORANT: false,
},
grapheme: "t",
ʰ: {
features: {},
grapheme: "tʰ",
},
},
u: {
features: {},
grapheme: "u",
},
z: {
features: {
PLOSIVE: false,
SONORANT: false,
},
grapheme: "z",
},
ə: {
features: {},
grapheme: "ə",
},
ɯ: {
features: {},
grapheme: "ɯ",
},
}
}
const lexiconDefinitionLatl = `
/PROTO
kpn
sm
/
`
const tokenizedLexicon = [
{ type: "slash", value: "/" }, { type: "referent", value: "PROTO" }, { type: 'lineBreak', value: '' },
{ type: "whiteSpace", value:"" }, { type: "referent", value: "kpn" }, { type: 'lineBreak', value: '' },
{ type: "whiteSpace", value:"" }, { type: "referent", value: "sm" }, { type: 'lineBreak', value: '' },
{ type: "slash", value: "/" }
]
const treeLexicon = {lexicon: [{epoch: "PROTO", value: ["kpn", "sm"]}]};
const lexiconState = {
...initState(),
latl: lexiconDefinitionLatl,
lexicon: [
{ lexeme: 'kpn', epoch: 'PROTO'},
{ lexeme: 'sm', epoch: 'PROTO'}
],
parseResults: 'latl parsed successfully'
}
const totalLatl = `${epochDefinitionLatl}\n\n${featureDefinitionLatl}\n\n${lexiconDefinitionLatl}`
const totalLatlState = {
...initState(),
latl: totalLatl,
phonemes: {},
features: featureState.features,
epochs: treeEpoch.epochs,
lexicon: lexiconState.lexicon,
parseResults: 'latl parsed successfully'
}

View file

@ -1,5 +1,5 @@
// @flow // @flow
import type { stateType } from './stateReducer'; import type { stateType } from './reducer';
type lexemeType = { type lexemeType = {
lexeme: string, lexeme: string,
@ -20,8 +20,10 @@ const makeLexeme = (lexeme: string, epochName: ?string, state: stateType) => {
const newLexeme = {lexeme: lexeme, epoch: state.epochs[0]}; const newLexeme = {lexeme: lexeme, epoch: state.epochs[0]};
if (epochName) { if (epochName) {
const epochIndex = state.epochs.findIndex(epoch => epoch.name === epochName); const epochIndex = state.epochs.findIndex(epoch => epoch.name === epochName);
if (epochIndex > 0) { if (epochIndex > -1) {
newLexeme.epoch = state.epochs[epochIndex]; newLexeme.epoch = state.epochs[epochIndex];
} else {
newLexeme.epoch = epochName;
}; };
} }
return newLexeme; return newLexeme;

View file

@ -1,10 +1,10 @@
import {stateReducer} from './stateReducer'; import {stateReducer} from './reducer';
describe('Lexicon', () => { describe('Lexicon', () => {
const state = { const state = {
epochs: [ epochs: [
{ name: 'epoch 1', changes:[''] }, { name: 'epoch-1', changes:[''] },
{ name: 'epoch 2', changes:[''] } { name: 'epoch-2', changes:[''] }
] ]
} }
state.lexicon = [ state.lexicon = [
@ -28,16 +28,16 @@ describe('Lexicon', () => {
}); });
it('lexicon addition with epoch returns updated lexicon with correct epoch', () => { it('lexicon addition with epoch returns updated lexicon with correct epoch', () => {
const action = {type: 'ADD_LEXEME', value: {lexeme:'ntʰa', epoch: 'epoch 2'}} const action = {type: 'ADD_LEXEME', value: {lexeme:'ntʰa', epoch: 'epoch-2'}}
expect(stateReducer(state, action)).toEqual({...state, lexicon:[...state.lexicon, {lexeme:'ntʰa', epoch:state.epochs[1]}]}); expect(stateReducer(state, action)).toEqual({...state, lexicon:[...state.lexicon, {lexeme:'ntʰa', epoch:state.epochs[1]}]});
}); });
it('lexicon set returns updated lexicon with correct epoch', () => { it('lexicon set returns updated lexicon with correct epoch', () => {
const newLexicon = [ const newLexicon = [
{lexeme:'anta', epoch:'epoch 1'}, {lexeme:'anta', epoch:'epoch-1'},
{lexeme:'anat', epoch:'epoch 1'}, {lexeme:'anat', epoch:'epoch-1'},
{lexeme:'anət', epoch:'epoch 1'}, {lexeme:'anət', epoch:'epoch-1'},
{lexeme:'anna', epoch:'epoch 1'} {lexeme:'anna', epoch:'epoch-1'}
] ]
const action = {type: 'SET_LEXICON', value: newLexicon} const action = {type: 'SET_LEXICON', value: newLexicon}
expect(stateReducer(state, action)).toEqual({...state, lexicon:[ expect(stateReducer(state, action)).toEqual({...state, lexicon:[
@ -58,7 +58,7 @@ describe('Lexicon', () => {
const inputLexicon = [ const inputLexicon = [
{lexeme:'anta'}, {lexeme:'anta'},
{lexeme:'anat'}, {lexeme:'anat'},
{lexeme:'anət', epoch:'epoch 2'}, {lexeme:'anət', epoch:'epoch-2'},
{lexeme:'anna'} {lexeme:'anna'}
] ]
const action = {type: 'SET_LEXICON', value: inputLexicon} const action = {type: 'SET_LEXICON', value: inputLexicon}

View file

@ -0,0 +1,20 @@
// @flow
import type { stateType } from './reducer';
export type optionAction = {
type: 'SET_OPTIONS',
value: {
option: string,
setValue: string
}
};
export const setOptions = (state: stateType, action: optionAction): stateType => {
const option = action.value.option;
let value = action.value.setValue;
if (value === 'true') value = true;
if (value === 'false') value = false;
const mutatedState = {...state};
mutatedState.options[option] = value;
return mutatedState;
}

View file

@ -0,0 +1,38 @@
import { stateReducer } from './reducer';
import { initState } from './reducer.init';
describe('Options', () => {
let state = {}
beforeEach(() => {
state = initState();
});
it('Options returned unaltered', () => {
const action = {type: ''};
expect(stateReducer(state, action)).toBe(state);
});
// output: 'default', save: false
it('Options change to output returns with changed value', () => {
const action = {type: 'SET_OPTIONS', value: {option: 'output', setValue: 'proto'}};
expect(stateReducer(state, action)).toEqual(
{...state,
options: {...state.options,
output: 'proto'
}
}
);
});
it('Options change to save returns with changed value', () => {
const action = {type: 'SET_OPTIONS', value: {option: 'save', setValue: 'true'}};
expect(stateReducer(state, action)).toEqual(
{...state,
options: {...state.options,
save: true
}
}
);
});
});

View file

@ -1,4 +1,4 @@
import {stateReducer} from './stateReducer'; import {stateReducer} from './reducer';
describe('Phones', () => { describe('Phones', () => {
const n_phone = {features: {nasal: true}, grapheme: 'n'}; const n_phone = {features: {nasal: true}, grapheme: 'n'};

View file

@ -0,0 +1,293 @@
// @flow
import type { stateType, epochType, phoneType } from './reducer';
export type resultsAction = {
type: 'RUN'
}
export type decomposedRulesType = [
{
environment: {
pre: [{[key: string]: boolean}],
position: [{[key: string]: boolean}],
post: [{[key: string]: boolean}]
},
newFeatures: [{[key: string]: boolean}]
}
]
type ruleBundle = {
environment: {
pre: string,
position: string,
post: string
},
newFeatures: string
}
const getProperty = property => object => object[property]
const findFeaturesFromLexeme = (phones: {}, lexeme:string): [] => {
let featureBundle = []
let lastIndex = lexeme.length - 1;
let node = {};
[...lexeme].forEach((graph, index) => {
try {
if (!index ) return node = phones[graph]
if (index === lastIndex) return node[graph]
? featureBundle.push(node[graph])
: featureBundle.push(node, phones[graph])
if (!node[graph] && node.features) {
featureBundle.push(node)
return node = phones[graph]
}
if (!node) return node = phones[graph]
return node = node[graph]
}
catch (e) {
throw {e, 'phones[graph]':phones[graph], index, lexeme }
}
})
return featureBundle;
}
const findFeaturesFromGrapheme = (phones: {}, lexeme:string): [] => {
let featureBundle = []
let lastIndex = lexeme.length - 1;
let node = {};
[...lexeme].forEach((graph, index) => {
if (!index && !lastIndex) featureBundle.push(phones[graph].features)
if (!index) return node = phones[graph]
if (index === lastIndex) return node[graph]
? featureBundle.push(node[graph])
: featureBundle.push(node, phones[graph])
if (!node[graph] && node.features) {
featureBundle.push(node)
return node = phones[graph]
}
if (!node[graph])
return node = node[graph]
})
return featureBundle;
}
const errorMessage = ([prefix, separator], location, err) => `${prefix}${location}${separator}${err}`
const lintRule = (rule) => {
if (!rule.match(/>/g)) throw `Insert '>' operator between target and result`
if (!rule.match(/\//g)) throw `Insert '/' operator between change and environment`
if (!rule.match(/_/g)) throw `Insert '_' operator in environment`
if (rule.match(/>/g).length > 1) throw `Too many '>' operators`
if (rule.match(/\//g).length > 1) throw `Too many '/' operators`
if (rule.match(/_/g).length > 1) throw `Too many '_' operators`
return rule.split(/>|\/|_/g);
}
const decomposeRule = (rule: string, index: number): ruleBundle => {
try {
// splits rule at '>' '/' and '_' substrings resulting in array of length 4
const [position, newFeatures, pre, post] = lintRule(rule);
return { environment: { pre, position, post }, newFeatures }
} catch (err) {
throw errorMessage`Error in line ${index + 1}: ${err}`;
}
}
const isUnknownFeatureToken = token => token !== '-' && token !== '+' && token !== ']' && token !== '[' && token !== ' ';
const doesFeatureRuleContainUnknownToken = features => {
const unknownTokens = features
.match(/\W/g)
.filter(isUnknownFeatureToken)
if (unknownTokens.length) throw `Unknown token '${unknownTokens[0]}'`;
return true
}
const reduceFeaturesToBoolean = bool => (map, feature) => ({...map, [feature]: bool})
const getFeatures = (phoneme: string, featureBoolean): {} => {
try {
const featureMatch = featureBoolean
// regEx to pull positive features
? /(?=\+.).*(?<=\-)|(?=\+.).*(?!\-).*(?<=\])/g
// regEx to pull negative features
: /(?=\-.).*(?<=\+)|(?=\-.).*(?!\+).*(?<=\])/g
const [ features ] = phoneme.match(featureMatch) || [ null ];
if (features) {
doesFeatureRuleContainUnknownToken(features)
return features
.trim()
.match(/\w+/g)
.reduce(reduceFeaturesToBoolean(featureBoolean), {})
}
return {}
} catch (err) {
throw err;
}
}
const mapToPositiveAndNegativeFeatures = phoneme => (
{ ...getFeatures(phoneme, true), ...getFeatures(phoneme, false) } )
const mapStringToFeatures = (ruleString, phones) => {
if (ruleString) {
if (ruleString === '.') return [];
if (ruleString === '#') return ['#']
if (ruleString === '0') return [];
const ruleBrackets = ruleString.match(/\[.*\]/)
try {
if (ruleBrackets) {
return ruleString
.split('[')
// filter out empty strings
.filter(v => v)
.map(mapToPositiveAndNegativeFeatures)
}
return findFeaturesFromGrapheme(phones, ruleString);
} catch (err) {
throw err;
}
}
return {};
}
const mapRuleBundleToFeatureBundle = phones => ( ruleBundle, index ) => {
// for each object in ruleBundle, map values to array of objects with feature-boolean key-value pairs
try {
const { newFeatures, environment:{ pre, position, post } } = ruleBundle;
return {
environment: {
pre: mapStringToFeatures(pre, phones),
position: mapStringToFeatures(position, phones),
post: mapStringToFeatures(post, phones),
},
newFeatures: mapStringToFeatures(newFeatures, phones)
}
} catch (err) {
throw errorMessage`Error in line ${index + 1}: ${err}`;
}
}
export const decomposeRules = (epoch: epochType, phones: {[key: string]: phoneType}): decomposedRulesType => {
const { changes } = epoch
try {
return changes
.map(decomposeRule)
.map(mapRuleBundleToFeatureBundle(phones));
} catch (err) {
const ruleError = {epoch: epoch.name, error: err}
throw ruleError;
}
}
const isPhonemeBoundByRule = phonemeFeatures => (ruleFeature, index) => {
const phoneme = phonemeFeatures[index].features;
return Object.entries(ruleFeature).reduce((bool, [feature, value]) => {
if (!bool) return false;
if (!phoneme.hasOwnProperty(feature)) return false;
if (!phoneme[feature] && !value) return true;
if (phoneme[feature] !== value) return false;
return true;
}, true);
}
const isEnvironmentBoundByRule = (phonemeFeatures, ruleFeatures) => {
if (!ruleFeatures) return true;
return ruleFeatures.filter(isPhonemeBoundByRule(phonemeFeatures)).length === ruleFeatures.length;
}
const getEntries = object => Object.entries(object);
const isObjectWithPropertyInArray = (array, property) => candidate => array.map(getProperty(property)).includes(candidate[property]);
const transformFeatureValues = features => ([newFeature, newValue]) => features[newFeature][newValue ? 'positive': 'negative'];
const reduceFeatureValues = (newPhoneme, [newFeature, newValue]) => ({ ...newPhoneme, [newFeature]: newValue })
const transformPhoneme = (phoneme, newFeatures, features) => {
if (!newFeatures) return {}
const newPhonemeFeatures = getEntries(newFeatures).reduce(reduceFeatureValues, {...phoneme.features});
const newPhonemeCandidates = getEntries(newPhonemeFeatures).map(transformFeatureValues(features));
return newPhonemeCandidates
.reduce((candidates, candidatesSubset, index, array) => candidates.filter(isObjectWithPropertyInArray(candidatesSubset, 'grapheme'))
, newPhonemeCandidates[newPhonemeCandidates.length - 1])[0];
}
const transformLexemeInitial = (newLexeme, pre, post, position, phoneme, index, lexemeBundle, newFeatures, features) => {
if (index !== pre.length - 1) return [...newLexeme, phoneme];
if (!isEnvironmentBoundByRule([phoneme], position)) return [...newLexeme, phoneme];
if (!isEnvironmentBoundByRule(lexemeBundle.slice(index + position.length, index + post.length + position.length), post)) return [...newLexeme, phoneme];
const newPhoneme = transformPhoneme(phoneme, newFeatures[0], features);
// if deletion occurs
if (!newPhoneme || !newPhoneme.grapheme) return [ ...newLexeme] ;
return [...newLexeme, newPhoneme];
}
const transformLexemeCoda = (newLexeme, pre, post, position, phoneme, index, lexemeBundle, newFeatures, features) => {
if (index + post.length !== lexemeBundle.length) return [...newLexeme, phoneme];
if (!isEnvironmentBoundByRule(lexemeBundle.slice(index - pre.length, index), pre)) return [...newLexeme, phoneme];
if (!isEnvironmentBoundByRule([phoneme], position)) return [...newLexeme, phoneme];
const newPhoneme = transformPhoneme(phoneme, newFeatures[0], features);
// if deletion occurs
if (!newPhoneme || !newPhoneme.grapheme) return [ ...newLexeme] ;
return [...newLexeme, newPhoneme];
}
export const transformLexeme = (lexemeBundle, rule, features) => {
const {pre, post, position} = rule.environment;
const newLexeme = lexemeBundle.reduce((newLexeme, phoneme, index) => {
if (pre.find(val => val === '#')) return transformLexemeInitial(newLexeme, pre, post, position, phoneme, index, lexemeBundle, rule.newFeatures, features);
if (post.find(val => val === '#')) return transformLexemeCoda(newLexeme, pre, post, position, phoneme, index, lexemeBundle, rule.newFeatures, features);
if ( index < pre.length || index >= lexemeBundle.length - post.length ) return [...newLexeme, phoneme];
if (!isEnvironmentBoundByRule(lexemeBundle.slice(index - pre.length, index), pre)) return [...newLexeme, phoneme];
if (!isEnvironmentBoundByRule([phoneme], position)) return [...newLexeme, phoneme];
if (!isEnvironmentBoundByRule(lexemeBundle.slice(index, index + post.length), post)) return [...newLexeme, phoneme];
const newPhoneme = transformPhoneme(phoneme, rule.newFeatures[0], features);
// if deletion occurs
if (!newPhoneme || !newPhoneme.grapheme) return [ ...newLexeme] ;
return [...newLexeme, newPhoneme];
}, [])
return newLexeme;
}
const formBundleFromLexicon = lexicon => phones => lexicon.map(({lexeme}) => findFeaturesFromLexeme(phones, lexeme))
const transformLexicon = lexiconBundle =>
ruleBundle =>
features =>
lexiconBundle.map(lexemeBundle => ruleBundle.reduce(
(lexeme, rule, i) => transformLexeme(lexeme, rule, features)
, lexemeBundle
))
const getGraphemeFromEntry = ([_, phoneme]) => phoneme.grapheme
const stringifyLexeme = (lexeme) => lexeme.map(getProperty('grapheme')).join('')
const stringifyResults = ({lexicon, ...passResults}) => ({...passResults, lexicon: lexicon.map(stringifyLexeme)})
export const run = (state: stateType, action: resultsAction): stateType => {
// TODO iterate through each epoch
try {
const passResults = state.epochs.reduce((results, epoch, _) => {
const { phones, features, lexicon } = state;
let lexiconBundle;
if ( epoch.parent ) {
lexiconBundle = results.find(result => result.pass === epoch.parent)
}
if (!lexiconBundle) {
lexiconBundle = formBundleFromLexicon(lexicon)(phones);
}
else {
lexiconBundle = lexiconBundle.lexicon
}
const ruleBundle = decomposeRules(epoch, phones);
const passResults = transformLexicon(lexiconBundle)(ruleBundle)(features)
const pass = { pass: epoch.name, lexicon: passResults }
if ( epoch.parent ) pass.parent = epoch.parent;
return [...results, pass];
}, []);
const results = passResults.map(stringifyResults);
return {...state, results, errors: {}, parseResults: '' }
} catch (err) {
console.log(err)
return {...state, errors: err, results:[], parseResults: '' };
}
}

View file

@ -0,0 +1,318 @@
import { stateReducer } from './reducer';
import { initState } from './reducer.init';
import { decomposeRules, transformLexeme } from './reducer.results';
describe('Results', () => {
let state = {};
beforeEach(()=> {
state = {};
})
it('results returned unaltered', () => {
const action = {type: ''};
expect(stateReducer(state, action)).toBe(state);
});
it('rules decomposed properly', () => {
const { epochs, phones } = initState(1);
const result = getResult();
expect(decomposeRules(epochs[0], phones)).toStrictEqual(result);
});
it('rule without ">" returns helpful error message', () => {
const { phones } = initState();
const epoch = { name: 'error epoch', changes: [ 't/n/_' ] }
const errorMessage = {epoch: 'error epoch', error: "Error in line 1: Insert '>' operator between target and result"};
let receivedError;
try {
decomposeRules(epoch, phones)
}
catch (err) {
receivedError=err;
}
expect(receivedError).toStrictEqual(errorMessage);
})
it('rule with too many ">" returns helpful error message', () => {
const { phones } = initState();
const epoch = { name: 'error epoch', changes: [ 't>n>/_' ] }
const errorMessage = {epoch: 'error epoch', error: "Error in line 1: Too many '>' operators"};
let receivedError;
try {
decomposeRules(epoch, phones)
}
catch (err) {
receivedError=err;
}
expect(receivedError).toStrictEqual(errorMessage);
})
it('rule without "/" returns helpful error message', () => {
const { phones } = initState();
const epoch = { name: 'error epoch', changes: [ 't>n_' ] }
const errorMessage = {epoch: 'error epoch', error: "Error in line 1: Insert '/' operator between change and environment"};
let receivedError;
try {
decomposeRules(epoch, phones)
}
catch (err) {
receivedError=err;
}
expect(receivedError).toStrictEqual(errorMessage);
})
it('rule with too many "/" returns helpful error message', () => {
const { phones } = initState();
const epoch = { name: 'error epoch', changes: [ 't>n/_/' ] }
const errorMessage = {epoch: 'error epoch', error: "Error in line 1: Too many '/' operators"};
let receivedError;
try {
decomposeRules(epoch, phones)
}
catch (err) {
receivedError=err;
}
expect(receivedError).toStrictEqual(errorMessage);
})
it('rule without "_" returns helpful error message', () => {
const { phones } = initState();
const epoch = { name: 'error epoch', changes: [ 't>n/' ] }
const errorMessage = {epoch: 'error epoch', error: "Error in line 1: Insert '_' operator in environment"};
let receivedError;
try {
decomposeRules(epoch, phones)
}
catch (err) {
receivedError=err;
}
expect(receivedError).toStrictEqual(errorMessage);
})
it('rule with too many "_" returns helpful error message', () => {
const { phones } = initState();
const epoch = { name: 'error epoch', changes: [ 't>n/__' ] }
const errorMessage = {epoch: 'error epoch', error: "Error in line 1: Too many '_' operators"};
let receivedError;
try {
decomposeRules(epoch, phones)
}
catch (err) {
receivedError=err;
}
expect(receivedError).toStrictEqual(errorMessage);
})
it('rule with incorrect feature syntax returns helpful error message', () => {
const { phones } = initState();
const epoch = { name: 'error epoch', changes: [ '[+ occlusive - nasal = obstruent]>n/_' ] }
const errorMessage = {epoch: 'error epoch', error: "Error in line 1: Unknown token '='"};
let receivedError;
try {
decomposeRules(epoch, phones)
}
catch (err) {
receivedError=err;
}
expect(receivedError).toStrictEqual(errorMessage);
})
it('expect transform lexeme to apply rule to lexeme', () => {
const lexemeBundle = getlexemeBundle();
const resultsLexeme = [...lexemeBundle]
resultsLexeme[2] = lexemeBundle[1]
const rule = getRule();
expect(transformLexeme(lexemeBundle, rule, initState().features)).toEqual(resultsLexeme)
})
it('results returned from first sound change rule (feature matching)', () => {
const action = {type: 'RUN'};
state = initState(1)
expect(stateReducer(state, action).results).toEqual([
{
pass: 'epoch-1',
lexicon: [
'anna', 'anat', 'anət', 'anna', 'tan', 'ənna'
]
}
]);
});
it('results returned through second sound change rule (phoneme matching)', () => {
const action = {type: 'RUN'};
state = initState(2)
expect(stateReducer(state, action).results).toEqual([
{
pass: 'epoch-1',
lexicon: [
'annɯ', 'anat', 'anət', 'annɯ', 'tan', 'ənnɯ'
]
}
]);
});
it('results returned through third sound change rule (phoneme dropping)', () => {
const action = {type: 'RUN'};
state = initState(3)
expect(stateReducer(state, action).results).toEqual([
{
pass: 'epoch-1',
lexicon: [
'annɯ', 'anat', 'ant', 'annɯ', 'tan', 'nnɯ'
]
}
]);
});
it('results returned through fourth sound change rule (lexeme initial environment)', () => {
const action = {type: 'RUN'};
state = initState(4)
expect(stateReducer(state, action).results).toEqual([
{
pass: 'epoch-1',
lexicon: [
'annɯ', 'anat', 'ant', 'annɯ', 'tʰan', 'nnɯ'
]
}
]);
});
it('results returned through fifth sound change rule (lexeme final environment)', () => {
const action = {type: 'RUN'};
state = initState(5)
expect(stateReducer(state, action).results).toEqual([
{
pass: 'epoch-1',
lexicon: [
'annu', 'anat', 'ant', 'annu', 'tʰan', 'nnu'
]
}
]);
});
// it('results returned through sixth sound change rule (multi-phoneme target)', () => {
// const action = {type: 'RUN'};
// state = initState(6)
// expect(stateReducer(state, action).results).toEqual([
// {
// pass: 'epoch-1',
// lexicon: [
// 'annu', 'anta', 'ant', 'annu', 'tʰan', 'nnu'
// ]
// }
// ]);
// });
it('results returned for multiple epochs without parent epoch', () => {
const action = {type: 'RUN'};
state = initState(5);
const newEpoch = {
name: 'epoch-2',
changes: [
'[+ sonorant ]>0/#_.',
'n>0/#_n'
]
}
state.epochs = [ ...state.epochs, newEpoch ]
expect(stateReducer(state, action).results).toEqual([
{
pass: 'epoch-1',
lexicon: [
'annu', 'anat', 'ant', 'annu', 'tʰan', 'nnu'
]
},
{
pass: 'epoch-2',
lexicon: [
'nta', 'nat', 'nət', 'na', 'tan', 'nta'
]
}
])
})
it('results returned for multiple epochs with parent epoch', () => {
const action = {type: 'RUN'};
state = initState(5);
const newEpoch = {
name: 'epoch-2',
parent: 'epoch-1',
changes: [
'[+ sonorant ]>0/#_.'
]
}
state.epochs = [ ...state.epochs, newEpoch ]
expect(stateReducer(state, action).results).toEqual([
{
pass: 'epoch-1',
lexicon: [
'annu', 'anat', 'ant', 'annu', 'tʰan', 'nnu'
]
},
{
pass: 'epoch-2',
parent: 'epoch-1',
lexicon: [
'nnu', 'nat', 'nt', 'nnu', 'tʰan', 'nu'
]
}
])
})
});
const getlexemeBundle = () => ([
{
grapheme: 'a',
features: {
sonorant: true,
back: true,
low: true,
high: false,
rounded: false
}
},
{
grapheme: 'n',
features: { sonorant: true, nasal: true, occlusive: true, coronal: true }
},
{
grapheme: 't',
features: { occlusive: true, coronal: true, obstruent: true, nasal: false }
},
{
grapheme: 'a',
features: {
sonorant: true,
back: true,
low: true,
high: false,
rounded: false
}
}
])
const getRule = () => ({
environment: {
pre: [ { sonorant: true, nasal: true, occlusive: true, coronal: true } ],
position: [ { occlusive: true, nasal: false } ],
post: []
},
newFeatures: [ { occlusive: true, nasal: true } ]
})
const getResult = () => ([
{
environment: {
pre: [
{
sonorant: true, nasal: true, occlusive: true, coronal: true
}
],
position: [
{occlusive: true, nasal: false}
],
post: [],
},
newFeatures: [{occlusive: true, nasal: true}]
}
]);

View file

@ -1,4 +1,4 @@
import {stateReducer} from './stateReducer'; import {stateReducer} from './reducer';
it('default returns state unaltered', () => { it('default returns state unaltered', () => {
const state = {data: 'example'}; const state = {data: 'example'};

View file

@ -1,30 +0,0 @@
// @flow
import type { stateType } from './stateReducer';
export type epochAction = {
type: "ADD_EPOCH" | "SET_EPOCH",
value: {
index?: number,
name: string,
changes?: Array<string>
}
}
export const addEpoch = (state: stateType, action: epochAction): stateType => {
const newEpoch = { ...action.value, changes: ['']};
return {...state, epochs: [...state.epochs, newEpoch]}
}
export const setEpoch = (state: stateType, action: epochAction): stateType => {
let mutatedEpochs = state.epochs;
const index = action.value.index
if (!index) return state;
mutatedEpochs[index].name = action.value.name
? action.value.name
: mutatedEpochs[index].name;
mutatedEpochs[index].changes = action.value.changes
? action.value.changes
: mutatedEpochs[index].changes;
return {...state, epochs: [...mutatedEpochs]}
}

View file

@ -1,90 +0,0 @@
// @flow
import type { stateType } from './stateReducer';
export type initAction = {
type: "INIT"
}
export const initState = (changesArgument: number = -1): stateType => {
const state = {
epochs: [
{
name: 'epoch 1',
changes: [
'[+ occlusive - nasal]>[+ occlusive nasal]/n_',
'at>ta/_#',
'[+ sonorant - low rounded high back]>_/_',
'nn>nun/_',
'[+ nasal][+ obstruent]>[+ nasal obstruent aspirated ]/#_',
'[+ sonorant rounded]>[+ sonorant - rounded]/_#'
]
}
],
phones: {
a: {
grapheme: 'a', features: {
sonorant: true, back: true, low: true, high: false, rounded: false
}
},
u: {
grapheme: 'u', features: {
sonorant: true, back: true, low: false, high: true, rounded: true,
}
},
ɯ: {
grapheme: 'ɯ', features: {
sonorant: true, back: true, low: false, high: true, rounded: false,
}
},
ə: {
grapheme: 'ə', features: {
sonorant: true, low: false, rounded: false, high: false, back: false
}
},
t: {
grapheme: 't', features: {
occlusive: true, coronal: true, obstruent: true
},
ʰ: {
grapheme: 'tʰ', features: {
occlusive: true, coronal: true, obstruent: true, aspirated: true
}
}
},
n: {
grapheme: 'n', features: {
sonorant: true, nasal: true, occlusive: true, coronal: true
}
}
},
options: {},
results: {},
errors: {},
features: {},
lexicon: []
};
state.features = {
sonorant: { positive:[ state.phones.a, state.phones.u, state.phones.ɯ, state.phones.ə, state.phones.n], negative: [] },
back: { positive:[ state.phones.a, state.phones.u, state.phones.ɯ ], negative: [ state.phones.ə ] },
low: { positive:[ state.phones.a ], negative: [ state.phones.u, state.phones.ɯ, state.phones.ə ] },
high: { positive:[ state.phones.u, state.phones.ɯ ], negative: [ state.phones.a, state.phones.ə ] },
rounded: { positive:[ state.phones.u ], negative: [ state.phones.a, state.phones.ɯ, state.phones.ə ] },
occlusive: { positive:[ state.phones.t, state.phones.n, state.phones.t.ʰ ], negative: [] },
coronal: { positive:[ state.phones.t, state.phones.n, state.phones.t.ʰ ], negative: [] },
obstruent: { positive:[ state.phones.t, state.phones.n, state.phones.t.ʰ ], negative: [] },
nasal: { positive:[ state.phones.n ], negative: [] },
aspirated: { positive:[ state.phones.t.ʰ ], negative: [] },
}
state.lexicon = [
{lexeme: 'anta', epoch: state.epochs[0]},
{lexeme: 'anat', epoch: state.epochs[0]},
{lexeme: 'anət', epoch: state.epochs[0]},
{lexeme: 'anna', epoch: state.epochs[0]},
{lexeme: 'tan', epoch: state.epochs[0]},
{lexeme: 'ənta', epoch: state.epochs[0]}
]
if(changesArgument > -1) state.epochs[0].changes = state.epochs[0].changes.splice(changesArgument, 1)
return state;
}

View file

@ -1,71 +0,0 @@
// @flow
import { addLexeme, setLexicon } from './stateReducer.lexicon';
import type { lexiconAction } from './stateReducer.lexicon';
import { addEpoch, setEpoch } from './stateReducer.epochs';
import type { epochAction } from './stateReducer.epochs';
import { addFeature } from './stateReducer.features';
import type { featureAction } from './stateReducer.features';
import { run } from './stateReducer.results';
import type { resultsAction } from './stateReducer.results'
import { initState } from './stateReducer.init';
import type { initAction } from './stateReducer.init';
export type stateType = {
lexicon: Array<{lexeme: string, epoch: epochType}>,
epochs: Array<epochType>,
phones: {[key: string]: phoneType},
options: {},
results: {},
errors: {},
features: featureType
}
type epochType = {
name: string, changes: Array<string>
}
type phoneType = {
grapheme: string,
features: {[key: string]: boolean}
}
type featureType = {
[key: string]: {[key: string]: Array<phoneType>}
}
type actionType = featureAction | epochAction | initAction | resultsAction | lexiconAction
export const stateReducer = (state: stateType, action: actionType): stateType => {
switch (action.type) {
case 'INIT': {
return initState();
}
case 'ADD_LEXEME': {
return addLexeme(state, action);
}
case 'SET_LEXICON': {
return setLexicon(state, action);
}
case 'ADD_EPOCH': {
return addEpoch(state, action);
}
case 'SET_EPOCH': {
return setEpoch(state, action);
}
case 'ADD_FEATURE': {
return addFeature(state, action);
}
case 'RUN': {
return run(state, action);
}
default:
return state;
}
}

View file

@ -1,57 +0,0 @@
// @flow
import type { stateType } from './stateReducer';
export type resultsAction = {
type: 'RUN'
}
const findFeatures = (phones: {}, lexeme:string): [] => {
let featureBundle = []
let lastIndex = lexeme.length - 1;
let node = {};
[...lexeme].forEach((graph, index) => {
if (!index) return node = phones[graph]
if (index === lastIndex) return node[graph]
? featureBundle.push(node[graph])
: featureBundle.push(node, phones[graph])
if (!node[graph] && node.features) {
featureBundle.push(node)
return node = phones[graph]
}
if (!node[graph])
return node = node[graph]
})
return featureBundle;
}
const decomposeRule = (rule: string): string[] => {
let decomposedChange = rule.split('>');
decomposedChange = [decomposedChange[0], ...decomposedChange[1].split('/')]
decomposedChange = [decomposedChange[0], decomposedChange[1], ...decomposedChange[2].split('_')];
return [...decomposedChange];
}
export const run = (state: stateType, action: resultsAction): stateType => {
// ! one epoch only
// rule 0 '[+ occlusive - nasal]>[+ occlusive nasal]/n_'
let ruleBundle = state.epochs[0].changes;
ruleBundle = ruleBundle.map(rule => decomposeRule(rule))
ruleBundle.map(rule => {
rule.forEach(position => {
console.log(position)
})
})
let featurePhoneBundle = state.lexicon.map(lexeme => findFeatures(state.phones, lexeme))
console.log(featurePhoneBundle)
ruleBundle.forEach(rule => {
featurePhoneBundle.map(featurePhone => {
// if (findRules(featurePhone, )
})
})
let results = [];
return {...state, results: { pass: state.epochs[0].name, results } }
}

View file

@ -1,26 +0,0 @@
import {stateReducer} from './stateReducer';
import {initState} from './stateReducer.init';
describe('Results', () => {
let state = {};
beforeEach(()=> {
state = {};
})
it('results returned unaltered', () => {
const action = {type: ''};
expect(stateReducer(state, action)).toBe(state);
});
it('results returned from first sound change rule', () => {
const action = {type: 'RUN'};
state = initState(0)
expect(stateReducer(state, action).results).toEqual({
pass: 'epoch 1',
results: [
'anna', 'anat', 'anət', 'anna', 'tan', 'ənna'
]
})
});
});

75
src/utils/latl/README.md Normal file
View file

@ -0,0 +1,75 @@
# LATL specification
## Feature Definition
## Rule Definition
ex.
```
(
`Unmotivated A to C`
A -> B / _
A -> C / _
``A becomes C in all environments with a intermediate state of B``
)
```
### Rule Body
#### Sound Definition
#### Change Definition
#### Environment Definition
##### Null Environment
Valid syntaxes:
```
A -> B ; no indicated environment
A -> B / _ ; environment indicated wth underscore
A -> B / . _ . ; environment indicated with underscore and placeholder dots
```
### Rule Metadata
#### Rule Title
#### Rule Description
## Language Primitives
## Data Structures
### Sets
Sets are collections of pointers to phones. The GLOBAL set contains all phones, making all other sets subsets of GLOBAL.
#### Global Set
[ GLOBAL ] is a shorthand for [ GLOBAL.SETS ]
#### Set Definition
Sets are defined with the set keyword followed by an equal sign and a set expression:
```
set SHORT_VOWELS = [ a, i, u ]
```
A single alias can be provided to the set during definition:
```
; the alias N can be used to refer to this set
set NASAL_PULMONIC_CONSONANTS, N = [ m, ɱ, n̼, n, ɳ, ɲ, ŋ, ɴ ]
```
Lists of sets can be defined using a comma followed by whitespace syntax
```
set PLOSIVES = [ p, t, k ],
FRICATIVES = [ f, s, x ],
LABIALIZED_PLOSIVES = { PLOSIVES yield [ X concat ʷ ] }
```
#### Set Usage
#### Set Operations
##### 'and' Operation
##### 'or' Operation
##### 'not' Operation
##### 'nor' Operation
##### 'in' Operation
##### 'yield' Operation
### Lexemes
#### Lexeme Operations
### Phone
For set of phones 'a', 'b', and 'ab':
```
GLOBAL ┬▻ <Key: a> ┬▻ <Key: b> ┬▻ { feature: <Boolean>, ... }
│ │ └▻ grapheme: <String: 'ab'>
│ └┬▻ { feature: <Boolean>, ... }
│ └▻ grapheme: <String: 'a'>
└┬▻ { feature: <Boolean>, ... }
└▻ grapheme: <String: 'b'>
```
#### Phone Operations
### Epochs

View file

@ -0,0 +1,19 @@
import { parser } from './parser';
export const codeGenerator = (latl) => {
const results = parser().feed(latl).results;
const nodeReader = (code, node) => {
if (node.length) {
return results.reduce(nodeReader, code)
}
if (!node) return code;
if (node.main) {
return nodeReader(code, node.main)
}
return code + node;
}
return nodeReader('', results)
}

120
src/utils/latl/grammar.js Normal file
View file

@ -0,0 +1,120 @@
// Generated automatically by nearley, version 2.19.1
// http://github.com/Hardmath123/nearley
(function () {
function id(x) { return x[0]; }
const { lexer } = require('./lexer.js');
const getTerminal = d => d ? d[0] : null;
const getAll = d => d.map((item, i) => ({ [i]: item }));
const flag = token => d => d.map(item => ({ [token]: item }))
const clearNull = d => d.filter(t => !!t && (t.length !== 1 || t[0])).map(t => t.length ? clearNull(t) : t);
const flagIndex = d => d.map((item, i) => ({[i]: item}))
const remove = _ => null;
const append = d => d.join('');
const constructSet = d => d.reduce((acc, t) => {
if (t && t.type === 'setIdentifier') acc.push({set: t});
if (t && t.length) acc[acc.length - 1].phones = t;
return acc;
}, []);
const pipe = (...funcs) => d => funcs.reduce((acc, func) => func(acc), d);
const objFromArr = d => d.reduce((obj, item) => ({ ...obj, ...item }), {});
var grammar = {
Lexer: lexer,
ParserRules: [
{"name": "main$ebnf$1", "symbols": []},
{"name": "main$ebnf$1$subexpression$1", "symbols": ["_", "statement"]},
{"name": "main$ebnf$1", "symbols": ["main$ebnf$1", "main$ebnf$1$subexpression$1"], "postprocess": function arrpush(d) {return d[0].concat([d[1]]);}},
{"name": "main", "symbols": ["main$ebnf$1", "_"], "postprocess": pipe(
clearNull,
// recursive call to fix repeat?
d => d.map(t => t && t.length === 1 && t[0] ? t[0] : t),
d => d.map(t => t && t.length === 1 && t[0] ? t[0] : t),
flag('main'),
getTerminal,
) },
{"name": "_$ebnf$1$subexpression$1", "symbols": [(lexer.has("whiteSpace") ? {type: "whiteSpace"} : whiteSpace)]},
{"name": "_$ebnf$1", "symbols": ["_$ebnf$1$subexpression$1"], "postprocess": id},
{"name": "_$ebnf$1", "symbols": [], "postprocess": function(d) {return null;}},
{"name": "_", "symbols": ["_$ebnf$1"], "postprocess": remove},
{"name": "__", "symbols": [(lexer.has("whiteSpace") ? {type: "whiteSpace"} : whiteSpace)], "postprocess": remove},
{"name": "equal", "symbols": [(lexer.has("equal") ? {type: "equal"} : equal)], "postprocess": remove},
{"name": "statement", "symbols": ["comment"]},
{"name": "statement", "symbols": ["definition"], "postprocess": pipe(
d => d.flatMap(u => u && u.length ? u.filter(t => t && t.type !== 'comma' && t.type !== 'kwSet') : u),
// recursive call to fit repeat?
d => d.map(t => t && t.length === 1 && t[0] ? t[0] : t),
d => d.map(t => t && t.length === 1 && t[0] ? t[0] : t),
// may split from other definition statements
d => d.map(t => t && t.length > 1 ? ({ type: 'set', ...objFromArr(t) }) : null)
) },
{"name": "comment", "symbols": [(lexer.has("comment") ? {type: "comment"} : comment)], "postprocess": pipe(getTerminal, remove)},
{"name": "definition$ebnf$1", "symbols": []},
{"name": "definition$ebnf$1$subexpression$1", "symbols": ["setDefinition", (lexer.has("comma") ? {type: "comma"} : comma), "__"]},
{"name": "definition$ebnf$1", "symbols": ["definition$ebnf$1", "definition$ebnf$1$subexpression$1"], "postprocess": function arrpush(d) {return d[0].concat([d[1]]);}},
{"name": "definition", "symbols": [(lexer.has("kwSet") ? {type: "kwSet"} : kwSet), "__", "definition$ebnf$1", "setDefinition"], "postprocess": pipe(
// not yet sure why this call is required twice
d => d.map(u => u && u.length ? u.filter(t => t && t.type !== 'comma' && t.type !== 'kwSet') : u),
d => d.map(u => u && u.length ? u.filter(t => t && t.type !== 'comma' && t.type !== 'kwSet') : u),
d => d.map(u => u && u.length ? u.map(v => v.length ? v.filter(t => t && t.type !== 'comma' && t.type !== 'kwSet')[0] : v) : u),
clearNull,
) },
{"name": "setDefinition$ebnf$1$subexpression$1", "symbols": ["setAlias"]},
{"name": "setDefinition$ebnf$1", "symbols": ["setDefinition$ebnf$1$subexpression$1"], "postprocess": id},
{"name": "setDefinition$ebnf$1", "symbols": [], "postprocess": function(d) {return null;}},
{"name": "setDefinition", "symbols": [(lexer.has("setIdentifier") ? {type: "setIdentifier"} : setIdentifier), "setDefinition$ebnf$1", "__", "equal", "__", "setExpression"], "postprocess":
pipe(
d => d.filter(t => !!t && t.length !== 0),
d => d.map(u => u && u.length ? u.map(t => t && t.length ? t.filter(v => v && v.type !== 'comma') : t) : u),
d => d.map(t => t.type === 'setIdentifier' ? { setIdentifier: t.toString() } : t),
d => d.map(t => t && t.length && t[0].hasOwnProperty('setExpression') ? t[0] : t),
d => d.map(t => t.length ?
// pretty ugly ([ { type: 'aias', alias: [ string ] }] ) => { setAlias: str }
{ setAlias: t.reduce((aliases, token) => token && token.type === 'alias' ? [...aliases, ...token.alias] : aliases, [])[0] }
: t),
)
},
{"name": "setExpression", "symbols": [(lexer.has("openSquareBracket") ? {type: "openSquareBracket"} : openSquareBracket), "_", "phoneList", "_", (lexer.has("closeSquareBracket") ? {type: "closeSquareBracket"} : closeSquareBracket)]},
{"name": "setExpression$ebnf$1$subexpression$1", "symbols": ["setOperation"]},
{"name": "setExpression$ebnf$1", "symbols": ["setExpression$ebnf$1$subexpression$1"], "postprocess": id},
{"name": "setExpression$ebnf$1", "symbols": [], "postprocess": function(d) {return null;}},
{"name": "setExpression", "symbols": [(lexer.has("openCurlyBracket") ? {type: "openCurlyBracket"} : openCurlyBracket), "_", "setExpression$ebnf$1", "_", (lexer.has("closeCurlyBracket") ? {type: "closeCurlyBracket"} : closeCurlyBracket)], "postprocess":
pipe(
// filters commas and whitespace
d => d.filter(t => t && t.length),
d => d.map(t => t.map(u => u[0])),
flag('setExpression')
) },
{"name": "setAlias", "symbols": [(lexer.has("comma") ? {type: "comma"} : comma), "_", (lexer.has("setIdentifier") ? {type: "setIdentifier"} : setIdentifier)], "postprocess": pipe(
d => d && d.length ? d.filter(t => !!t) : d,
d => d.map(t => t.type === 'setIdentifier' ? t.toString() : null),
d => d.filter(t => !!t),
d => ({type: 'alias', alias: d }),
) },
{"name": "phoneList$ebnf$1", "symbols": []},
{"name": "phoneList$ebnf$1$subexpression$1$ebnf$1", "symbols": []},
{"name": "phoneList$ebnf$1$subexpression$1$ebnf$1$subexpression$1", "symbols": [(lexer.has("comma") ? {type: "comma"} : comma), "_"]},
{"name": "phoneList$ebnf$1$subexpression$1$ebnf$1", "symbols": ["phoneList$ebnf$1$subexpression$1$ebnf$1", "phoneList$ebnf$1$subexpression$1$ebnf$1$subexpression$1"], "postprocess": function arrpush(d) {return d[0].concat([d[1]]);}},
{"name": "phoneList$ebnf$1$subexpression$1", "symbols": [(lexer.has("phone") ? {type: "phone"} : phone), "phoneList$ebnf$1$subexpression$1$ebnf$1"]},
{"name": "phoneList$ebnf$1", "symbols": ["phoneList$ebnf$1", "phoneList$ebnf$1$subexpression$1"], "postprocess": function arrpush(d) {return d[0].concat([d[1]]);}},
{"name": "phoneList", "symbols": ["phoneList$ebnf$1"], "postprocess":
pipe(
d => d ? d[0].map(t => t.filter(u => u.type === 'phone').map(u => u.toString())) : d
)
},
{"name": "setOperation", "symbols": ["orOperation"]},
{"name": "setOperation", "symbols": [(lexer.has("identifier") ? {type: "identifier"} : identifier)], "postprocess": pipe(
d => d.type ? d : ({ identifier: d.toString(), type: 'identifier' })
)},
{"name": "orOperation", "symbols": ["_", "setOperation", "__", (lexer.has("kwSetOr") ? {type: "kwSetOr"} : kwSetOr), "__", "setOperation", "_"], "postprocess": pipe(
d => d.filter(d => !!d),
d => ({ type: 'operator', operator: 'or', operands: [ d[0], d[2] ] }),
) }
]
, ParserStart: "main"
}
if (typeof module !== 'undefined'&& typeof module.exports !== 'undefined') {
module.exports = grammar;
} else {
window.grammar = grammar;
}
})();

109
src/utils/latl/grammar.ne Normal file
View file

@ -0,0 +1,109 @@
@{%
const { lexer } = require('./lexer.js');
const getTerminal = d => d ? d[0] : null;
const getAll = d => d.map((item, i) => ({ [i]: item }));
const flag = token => d => d.map(item => ({ [token]: item }))
const clearNull = d => d.filter(t => !!t && (t.length !== 1 || t[0])).map(t => t.length ? clearNull(t) : t);
const flagIndex = d => d.map((item, i) => ({[i]: item}))
const remove = _ => null;
const append = d => d.join('');
const constructSet = d => d.reduce((acc, t) => {
if (t && t.type === 'setIdentifier') acc.push({set: t});
if (t && t.length) acc[acc.length - 1].phones = t;
return acc;
}, []);
const pipe = (...funcs) => d => funcs.reduce((acc, func) => func(acc), d);
const objFromArr = d => d.reduce((obj, item) => ({ ...obj, ...item }), {});
%}
@lexer lexer
main -> (_ statement):* _
{% pipe(
clearNull,
// recursive call to fix repeat?
d => d.map(t => t && t.length === 1 && t[0] ? t[0] : t),
d => d.map(t => t && t.length === 1 && t[0] ? t[0] : t),
flag('main'),
getTerminal,
) %}
_ -> (%whiteSpace):?
{% remove %}
__ -> %whiteSpace
{% remove %}
equal -> %equal
{% remove %}
statement -> comment | definition
{% pipe(
d => d.flatMap(u => u && u.length ? u.filter(t => t && t.type !== 'comma' && t.type !== 'kwSet') : u),
// recursive call to fit repeat?
d => d.map(t => t && t.length === 1 && t[0] ? t[0] : t),
d => d.map(t => t && t.length === 1 && t[0] ? t[0] : t),
// may split from other definition statements
d => d.map(t => t && t.length > 1 ? ({ type: 'set', ...objFromArr(t) }) : null)
) %}
comment -> %comment
{% pipe(getTerminal, remove) %}
# SETS
definition -> %kwSet __ (setDefinition %comma __):* setDefinition
{% pipe(
// not yet sure why this call is required twice
d => d.map(u => u && u.length ? u.filter(t => t && t.type !== 'comma' && t.type !== 'kwSet') : u),
d => d.map(u => u && u.length ? u.filter(t => t && t.type !== 'comma' && t.type !== 'kwSet') : u),
d => d.map(u => u && u.length ? u.map(v => v.length ? v.filter(t => t && t.type !== 'comma' && t.type !== 'kwSet')[0] : v) : u),
clearNull,
) %}
setDefinition -> %setIdentifier (setAlias):? __ equal __ setExpression
{%
pipe(
d => d.filter(t => !!t && t.length !== 0),
d => d.map(u => u && u.length ? u.map(t => t && t.length ? t.filter(v => v && v.type !== 'comma') : t) : u),
d => d.map(t => t.type === 'setIdentifier' ? { setIdentifier: t.toString() } : t),
d => d.map(t => t && t.length && t[0].hasOwnProperty('setExpression') ? t[0] : t),
d => d.map(t => t.length ?
// pretty ugly ([ { type: 'aias', alias: [ string ] }] ) => { setAlias: str }
{ setAlias: t.reduce((aliases, token) => token && token.type === 'alias' ? [...aliases, ...token.alias] : aliases, [])[0] }
: t),
)
%}
setExpression -> %openSquareBracket _ phoneList _ %closeSquareBracket
| %openCurlyBracket _ (setOperation):? _ %closeCurlyBracket
{%
pipe(
// filters commas and whitespace
d => d.filter(t => t && t.length),
d => d.map(t => t.map(u => u[0])),
flag('setExpression')
) %}
setAlias -> %comma _ %setIdentifier
{% pipe(
d => d && d.length ? d.filter(t => !!t) : d,
d => d.map(t => t.type === 'setIdentifier' ? t.toString() : null),
d => d.filter(t => !!t),
d => ({type: 'alias', alias: d }),
) %}
phoneList -> (%phone (%comma _):* ):*
{%
pipe(
d => d ? d[0].map(t => t.filter(u => u.type === 'phone').map(u => u.toString())) : d
)
%}
setOperation -> orOperation
| %identifier
{% pipe(
d => d.type ? d : ({ identifier: d.toString(), type: 'identifier' })
)%}
orOperation -> _ setOperation __ %kwSetOr __ setOperation _
{% pipe(
d => d.filter(d => !!d),
d => ({ type: 'operator', operator: 'or', operands: [ d[0], d[2] ] }),
) %}

124
src/utils/latl/lexer.js Normal file
View file

@ -0,0 +1,124 @@
const moo = require("moo");
const lexer = moo.states({
main: {
comment: /;.*$/,
star: { match: /\*/, push: "epoch" },
slash: { match: /\//, push: "lexicon" },
// change so that identifiers are always upper, keywords are always lower, phones are always lower
kwSet: {
match: "set",
type: moo.keywords({ kwSet: "set " }),
push: "setDefinition",
},
identifier: { match: /[A-Za-z]+[\u00c0-\u03FFA-Za-z0-9\\-\\_]*/ },
openBracket: { match: /\[/, push: "feature" },
whiteSpace: { match: /\s+/, lineBreaks: true },
newLine: { match: /\n+/, lineBreaks: true },
},
epoch: {
identifier: {
match: /[A-Za-z]+[\u00c0-\u03FFA-Za-z0-9\\-\\_]*/,
push: "rule",
},
openParen: { match: /\(/, push: "ruleDefinition" },
pipe: { match: /\|/, pop: true },
greaterThan: /\>/,
arrow: /\-\>/,
hash: /#/,
slash: /\//,
dot: /\./,
underscore: /\_/,
newLine: { match: /\n/, lineBreaks: true },
},
ruleDefinition: {
doubleTick: { match: /``/, push: "ruleName" },
singleTick: { match: /`/, push: "ruleDescription" },
// push rule
closeParen: { match: /\)/, pop: true },
newLine: { match: /\n/, lineBreaks: true },
},
ruleName: {
ruleName: { match: /.+(?=``)/ },
doubleTick: { match: /``/, pop: true },
},
ruleDescription: {
ruleDescription: { match: /.+(?=`)/ },
singleTick: { match: /`/, pop: true },
},
rule: {
openSquareBracket: { match: /\[/, push: "ruleFeature" },
// whiteSpace: { match: /\s/ },
newLine: { match: /\n/, pop: true, lineBreaks: true },
},
ruleFeature: {
ruleFeature: { match: /[A-Za-z]+[\u00c0-\u03FFA-Za-z0-9\\-\\_]*/ },
closeBracket: { match: /\]/, pop: true },
newLine: { match: /\n/, lineBreaks: true },
},
lexicon: {
slash: { match: /\//, pop: true },
newLine: { match: /\n/, lineBreaks: true },
},
feature: {
closeBracket: { match: /\]/, pop: true },
positiveAssignment: /\+=/,
negativeAssignment: /\-=/,
newLine: { match: /\n/, lineBreaks: true },
},
setDefinition: {
comment: /;.*$/,
setIdentifier: { match: /[A-Z]+[A-Z_]*/ },
openCurlyBracket: { match: /\{/, push: "setOperation" },
equal: /=/,
openSquareBracket: /\[/,
phone: /[\u00c0-\u03FFa-z]+/,
closeSquareBracket: { match: /\]/ },
comma: { match: /,/, push: "commaOperation" },
whiteSpace: { match: /[\t ]+/ },
newLine: { match: /\n/, pop: true, lineBreaks: true },
},
setOperation: {
closeCurlyBracket: { match: /\}/, pop: true },
// ! restrict identifiers
keyword: {
match: ["not", "and", "or", "nor", "in", "yield", "concat", "dissoc"],
type: moo.keywords({
kwSetNot: "not",
kwSetAnd: "and",
kwSetOr: "or",
kwSetNor: "nor",
kwSetIn: "in",
kwSetYield: "yield",
kwSetConcat: "concat",
kwSetDissoc: "dissoc",
}),
},
identifier: /[A-Z]+[A-Z_]+/,
whiteSpace: { match: /\s+/, lineBreaks: true },
openSquareBracket: /\[/,
closeSquareBracket: /\]/,
identifier: /[A-Z]+[A-Z_]*/,
phone: /[\u00c0-\u03FFa-z]+/,
},
commaOperation: {
// if comma is detected during a definition, the commaOperation consumes all white space and pops back to definition
// this prevents popping back to main
comment: /\s*;.*$/,
whiteSpace: { match: /\s+/, lineBreaks: true, pop: true },
newLine: { match: /\n/, lineBreaks: true, pop: true },
},
});
module.exports = { lexer };

4
src/utils/latl/parser.js Normal file
View file

@ -0,0 +1,4 @@
const nearley = require("nearley");
const grammar = require("./grammar.js");
export const parser = () => new nearley.Parser(nearley.Grammar.fromCompiled(grammar));

View file

@ -0,0 +1,810 @@
export const assertionData = {
simpleComment: {
latl: `; comment`,
tokens: [{ type: "comment", value: "; comment" }],
AST: {
main: [],
},
code: "",
},
simpleSetDefinition: {
latl: `set NASAL_PULMONIC_CONSONANTS = [ m̥, m, ɱ ]`,
tokens: [
{ type: "kwSet", value: "set" },
{ type: "whiteSpace", value: " " },
{ type: "setIdentifier", value: "NASAL_PULMONIC_CONSONANTS" },
{ type: "whiteSpace", value: " " },
{ type: "equal", value: "=" },
{ type: "whiteSpace", value: " " },
{ type: "openSquareBracket", value: "[" },
{ type: "whiteSpace", value: " " },
{ type: "phone", value: "m̥" },
{ type: "comma", value: "," },
{ type: "whiteSpace", value: " " },
{ type: "phone", value: "m" },
{ type: "comma", value: "," },
{ type: "whiteSpace", value: " " },
{ type: "phone", value: "ɱ" },
{ type: "whiteSpace", value: " " },
{ type: "closeSquareBracket", value: "]" },
],
AST: {
main: [
{
type: "set",
setIdentifier: "NASAL_PULMONIC_CONSONANTS",
setExpression: ["m̥", "m", "ɱ"],
},
],
},
code: "",
},
commaSetDefinition: {
latl: `
set NASAL_PULMONIC_CONSONANTS = [ m̥, m, ɱ, n̼, n̥, n, ɳ̊, ɳ, ɲ̊, ɲ, ŋ, ̊ŋ, ɴ ],
STOP_PULMONIC_CONSONANTS = [ p, b, p̪, b̪, t̼, d̼, t, d, ʈ, ɖ, c, ɟ, k, ɡ, q, ɢ, ʡ, ʔ ]`,
tokens: [
{ type: "whiteSpace", value: "\n" },
{ type: "kwSet", value: "set" },
{ type: "whiteSpace", value: " " },
{ type: "setIdentifier", value: "NASAL_PULMONIC_CONSONANTS" },
{ type: "whiteSpace", value: " " },
{ type: "equal", value: "=" },
{ type: "whiteSpace", value: " " },
{ type: "openSquareBracket", value: "[" },
{ type: "whiteSpace", value: " " },
{ type: "phone", value: "m̥" },
{ type: "comma", value: "," },
{ type: "whiteSpace", value: " " },
{ type: "phone", value: "m" },
{ type: "comma", value: "," },
{ type: "whiteSpace", value: " " },
{ type: "phone", value: "ɱ" },
{ type: "comma", value: "," },
{ type: "whiteSpace", value: " " },
{ type: "phone", value: "n̼" },
{ type: "comma", value: "," },
{ type: "whiteSpace", value: " " },
{ type: "phone", value: "n̥" },
{ type: "comma", value: "," },
{ type: "whiteSpace", value: " " },
{ type: "phone", value: "n" },
{ type: "comma", value: "," },
{ type: "whiteSpace", value: " " },
{ type: "phone", value: "ɳ̊" },
{ type: "comma", value: "," },
{ type: "whiteSpace", value: " " },
{ type: "phone", value: "ɳ" },
{ type: "comma", value: "," },
{ type: "whiteSpace", value: " " },
{ type: "phone", value: "ɲ̊" },
{ type: "comma", value: "," },
{ type: "whiteSpace", value: " " },
{ type: "phone", value: "ɲ" },
{ type: "comma", value: "," },
{ type: "whiteSpace", value: " " },
{ type: "phone", value: "ŋ" },
{ type: "comma", value: "," },
{ type: "whiteSpace", value: " " },
{ type: "phone", value: "̊ŋ" },
{ type: "comma", value: "," },
{ type: "whiteSpace", value: " " },
{ type: "phone", value: "ɴ" },
{ type: "whiteSpace", value: " " },
{ type: "closeSquareBracket", value: "]" },
{ type: "comma", value: "," },
{ type: "whiteSpace", value: "\n " },
{ type: "setIdentifier", value: "STOP_PULMONIC_CONSONANTS" },
{ type: "whiteSpace", value: " " },
{ type: "equal", value: "=" },
{ type: "whiteSpace", value: " " },
{ type: "openSquareBracket", value: "[" },
{ type: "whiteSpace", value: " " },
{ type: "phone", value: "p" },
{ type: "comma", value: "," },
{ type: "whiteSpace", value: " " },
{ type: "phone", value: "b" },
{ type: "comma", value: "," },
{ type: "whiteSpace", value: " " },
{ type: "phone", value: "p̪" },
{ type: "comma", value: "," },
{ type: "whiteSpace", value: " " },
{ type: "phone", value: "b̪" },
{ type: "comma", value: "," },
{ type: "whiteSpace", value: " " },
{ type: "phone", value: "t̼" },
{ type: "comma", value: "," },
{ type: "whiteSpace", value: " " },
{ type: "phone", value: "d̼" },
{ type: "comma", value: "," },
{ type: "whiteSpace", value: " " },
{ type: "phone", value: "t" },
{ type: "comma", value: "," },
{ type: "whiteSpace", value: " " },
{ type: "phone", value: "d" },
{ type: "comma", value: "," },
{ type: "whiteSpace", value: " " },
{ type: "phone", value: "ʈ" },
{ type: "comma", value: "," },
{ type: "whiteSpace", value: " " },
{ type: "phone", value: "ɖ" },
{ type: "comma", value: "," },
{ type: "whiteSpace", value: " " },
{ type: "phone", value: "c" },
{ type: "comma", value: "," },
{ type: "whiteSpace", value: " " },
{ type: "phone", value: "ɟ" },
{ type: "comma", value: "," },
{ type: "whiteSpace", value: " " },
{ type: "phone", value: "k" },
{ type: "comma", value: "," },
{ type: "whiteSpace", value: " " },
{ type: "phone", value: "ɡ" },
{ type: "comma", value: "," },
{ type: "whiteSpace", value: " " },
{ type: "phone", value: "q" },
{ type: "comma", value: "," },
{ type: "whiteSpace", value: " " },
{ type: "phone", value: "ɢ" },
{ type: "comma", value: "," },
{ type: "whiteSpace", value: " " },
{ type: "phone", value: "ʡ" },
{ type: "comma", value: "," },
{ type: "whiteSpace", value: " " },
{ type: "phone", value: "ʔ" },
{ type: "whiteSpace", value: " " },
{ type: "closeSquareBracket", value: "]" },
],
AST: {
main: [
{
type: "set",
setIdentifier: "NASAL_PULMONIC_CONSONANTS",
setExpression: [
"m̥",
"m",
"ɱ",
"n̼",
"n̥",
"n",
"ɳ̊",
"ɳ",
"ɲ̊",
"ɲ",
"ŋ",
"̊ŋ",
"ɴ",
],
},
{
type: "set",
setIdentifier: "STOP_PULMONIC_CONSONANTS",
setExpression: [
"p",
"b",
"p̪",
"b̪",
"t̼",
"d̼",
"t",
"d",
"ʈ",
"ɖ",
"c",
"ɟ",
"k",
"ɡ",
"q",
"ɢ",
"ʡ",
"ʔ",
],
},
],
},
},
setAliasDefinition: {
latl: `
set NASAL_PULMONIC_CONSONANTS, N = [ m̥, m, ɱ, n̼, n̥, n, ɳ̊, ɳ, ɲ̊, ɲ, ŋ, ̊ŋ, ɴ ]`,
tokens: [
{ type: "whiteSpace", value: "\n" },
{ type: "kwSet", value: "set" },
{ type: "whiteSpace", value: " " },
{ type: "setIdentifier", value: "NASAL_PULMONIC_CONSONANTS" },
{ type: "comma", value: "," },
{ type: "whiteSpace", value: " " },
{ type: "setIdentifier", value: "N" },
{ type: "whiteSpace", value: " " },
{ type: "equal", value: "=" },
{ type: "whiteSpace", value: " " },
{ type: "openSquareBracket", value: "[" },
{ type: "whiteSpace", value: " " },
{ type: "phone", value: "m̥" },
{ type: "comma", value: "," },
{ type: "whiteSpace", value: " " },
{ type: "phone", value: "m" },
{ type: "comma", value: "," },
{ type: "whiteSpace", value: " " },
{ type: "phone", value: "ɱ" },
{ type: "comma", value: "," },
{ type: "whiteSpace", value: " " },
{ type: "phone", value: "n̼" },
{ type: "comma", value: "," },
{ type: "whiteSpace", value: " " },
{ type: "phone", value: "n̥" },
{ type: "comma", value: "," },
{ type: "whiteSpace", value: " " },
{ type: "phone", value: "n" },
{ type: "comma", value: "," },
{ type: "whiteSpace", value: " " },
{ type: "phone", value: "ɳ̊" },
{ type: "comma", value: "," },
{ type: "whiteSpace", value: " " },
{ type: "phone", value: "ɳ" },
{ type: "comma", value: "," },
{ type: "whiteSpace", value: " " },
{ type: "phone", value: "ɲ̊" },
{ type: "comma", value: "," },
{ type: "whiteSpace", value: " " },
{ type: "phone", value: "ɲ" },
{ type: "comma", value: "," },
{ type: "whiteSpace", value: " " },
{ type: "phone", value: "ŋ" },
{ type: "comma", value: "," },
{ type: "whiteSpace", value: " " },
{ type: "phone", value: "̊ŋ" },
{ type: "comma", value: "," },
{ type: "whiteSpace", value: " " },
{ type: "phone", value: "ɴ" },
{ type: "whiteSpace", value: " " },
{ type: "closeSquareBracket", value: "]" },
],
AST: {
main: [
{
type: "set",
setIdentifier: "NASAL_PULMONIC_CONSONANTS",
setAlias: "N",
setExpression: [
"m̥",
"m",
"ɱ",
"n̼",
"n̥",
"n",
"ɳ̊",
"ɳ",
"ɲ̊",
"ɲ",
"ŋ",
"̊ŋ",
"ɴ",
],
},
],
},
},
setDefinitionJoin: {
latl: `
set CLICK_CONSONANTS = { TENUIS_CLICK_CONSONANTS or VOICED_CLICK_CONSONANTS }`,
tokens: [
{ type: "whiteSpace", value: "\n" },
{ type: "kwSet", value: "set" },
{ type: "whiteSpace", value: " " },
{ type: "setIdentifier", value: "CLICK_CONSONANTS" },
{ type: "whiteSpace", value: " " },
{ type: "equal", value: "=" },
{ type: "whiteSpace", value: " " },
{ type: "openCurlyBracket", value: "{" },
{ type: "whiteSpace", value: " " },
{ type: "identifier", value: "TENUIS_CLICK_CONSONANTS" },
{ type: "whiteSpace", value: " " },
{ type: "kwSetOr", value: "or" },
{ type: "whiteSpace", value: " " },
{ type: "identifier", value: "VOICED_CLICK_CONSONANTS" },
{ type: "whiteSpace", value: " " },
{ type: "closeCurlyBracket", value: "}" },
],
AST: {
main: [
{
type: "set",
setIdentifier: "CLICK_CONSONANTS",
setExpression: [
{
type: "operator",
operator: "or",
operands: [
{
type: "identifier",
identifier: "TENUIS_CLICK_CONSONANTS",
},
{
type: "identifier",
identifier: "VOICED_CLICK_CONSONANTS",
},
],
},
],
},
],
},
},
setDefinitionMultiJoin: {
latl: `
set CLICK_CONSONANTS = { TENUIS_CLICK_CONSONANTS or VOICED_CLICK_CONSONANTS
or NASAL_CLICK_CONSONANTS or L_CLICK_CONSONANTS
}`,
tokens: [
{ type: "whiteSpace", value: "\n" },
{ type: "kwSet", value: "set" },
{ type: "whiteSpace", value: " " },
{ type: "setIdentifier", value: "CLICK_CONSONANTS" },
{ type: "whiteSpace", value: " " },
{ type: "equal", value: "=" },
{ type: "whiteSpace", value: " " },
{ type: "openCurlyBracket", value: "{" },
{ type: "whiteSpace", value: " " },
{ type: "identifier", value: "TENUIS_CLICK_CONSONANTS" },
{ type: "whiteSpace", value: " " },
{ type: "kwSetOr", value: "or" },
{ type: "whiteSpace", value: " " },
{ type: "identifier", value: "VOICED_CLICK_CONSONANTS" },
{ type: "whiteSpace", value: "\n " },
{ type: "kwSetOr", value: "or" },
{ type: "whiteSpace", value: " " },
{ type: "identifier", value: "NASAL_CLICK_CONSONANTS" },
{ type: "whiteSpace", value: " " },
{ type: "kwSetOr", value: "or" },
{ type: "whiteSpace", value: " " },
{ type: "identifier", value: "L_CLICK_CONSONANTS" },
{ type: "whiteSpace", value: " \n " },
{ type: "closeCurlyBracket", value: "}" },
],
AST: {
main: [
{
type: "set",
setIdentifier: "CLICK_CONSONANTS",
setExpression: [
{
type: "operator",
operator: "or ",
operands: [
{
type: "identifier",
identifier: "TENUIS_CLICK_CONSONANTS",
},
{
type: "operator",
operator: "or",
operands: [
{
type: "identifier",
identifier: "VOICED_CLICK_CONSONANTS",
},
{
type: "operator",
operator: "or",
operands: [
{
type: "identifier",
identifier: "NASAL_CLICK_CONSONANTS",
},
{
type: "identifier",
operands: "L_CLICK_CONSONANTS",
},
],
},
],
},
],
},
],
},
],
},
},
setDefinitionYield: {
latl: `
set NASAL_VOWELS = { [ V ] in ORAL_VOWELS yield [ Ṽ ] },
SHORT_NASAL_VOWELS = { [ Vː ] in NASAL_VOWELS yield [ V ]ː },
LONG_NASAL_VOWELS = { [ Vː ] in NASAL_VOWELS }`,
tokens: [
{ type: "whiteSpace", value: "\n" },
{ type: "kwSet", value: "set" },
{ type: "whiteSpace", value: " " },
{ type: "setIdentifier", value: "NASAL_VOWELS" },
{ type: "whiteSpace", value: " " },
{ type: "equal", value: "=" },
{ type: "whiteSpace", value: " " },
{ type: "openCurlyBracket", value: "{" },
{ type: "whiteSpace", value: " " },
{ type: "openSquareBracket", value: "[" },
{ type: "whiteSpace", value: " " },
{ type: "identifier", value: "V" },
{ type: "whiteSpace", value: " " },
{ type: "closeSquareBracket", value: "]" },
{ type: "whiteSpace", value: " " },
{ type: "kwSetIn", value: "in" },
{ type: "whiteSpace", value: " " },
{ type: "identifier", value: "ORAL_VOWELS" },
{ type: "whiteSpace", value: " " },
{ type: "kwSetYield", value: "yield" },
{ type: "whiteSpace", value: " " },
{ type: "openSquareBracket", value: "[" },
{ type: "whiteSpace", value: " " },
{ type: "identifier", value: "V" },
{ type: "phone", value: "̃" }, // test display for COMBINING TILDE OVERLAY is deceiving
{ type: "whiteSpace", value: " " },
{ type: "closeSquareBracket", value: "]" },
{ type: "whiteSpace", value: " " },
{ type: "closeCurlyBracket", value: "}" },
{ type: "comma", value: "," },
{ type: "whiteSpace", value: "\n " },
{ type: "setIdentifier", value: "SHORT_NASAL_VOWELS" },
{ type: "whiteSpace", value: " " },
{ type: "equal", value: "=" },
{ type: "whiteSpace", value: " " },
{ type: "openCurlyBracket", value: "{" },
{ type: "whiteSpace", value: " " },
{ type: "openSquareBracket", value: "[" },
{ type: "whiteSpace", value: " " },
{ type: "identifier", value: "V" },
{ type: "phone", value: "ː" },
{ type: "whiteSpace", value: " " },
{ type: "closeSquareBracket", value: "]" },
{ type: "whiteSpace", value: " " },
{ type: "kwSetIn", value: "in" },
{ type: "whiteSpace", value: " " },
{ type: "identifier", value: "NASAL_VOWELS" },
{ type: "whiteSpace", value: " " },
{ type: "kwSetYield", value: "yield" },
{ type: "whiteSpace", value: " " },
{ type: "openSquareBracket", value: "[" },
{ type: "whiteSpace", value: " " },
{ type: "identifier", value: "V" },
{ type: "whiteSpace", value: " " },
{ type: "closeSquareBracket", value: "]" },
{ type: "phone", value: "ː" },
{ type: "whiteSpace", value: " " },
{ type: "closeCurlyBracket", value: "}" },
{ type: "comma", value: "," },
{ type: "whiteSpace", value: "\n " },
{ type: "setIdentifier", value: "LONG_NASAL_VOWELS" },
{ type: "whiteSpace", value: " " },
{ type: "equal", value: "=" },
{ type: "whiteSpace", value: " " },
{ type: "openCurlyBracket", value: "{" },
{ type: "whiteSpace", value: " " },
{ type: "openSquareBracket", value: "[" },
{ type: "whiteSpace", value: " " },
{ type: "identifier", value: "V" },
{ type: "phone", value: "ː" },
{ type: "whiteSpace", value: " " },
{ type: "closeSquareBracket", value: "]" },
{ type: "whiteSpace", value: " " },
{ type: "kwSetIn", value: "in" },
{ type: "whiteSpace", value: " " },
{ type: "identifier", value: "NASAL_VOWELS" },
{ type: "whiteSpace", value: " " },
{ type: "closeCurlyBracket", value: "}" },
],
},
setOperationsJoin: {
latl: `
; ---- set join operations non-mutable!
set SET_C = { SET_A not SET_B }, ; left anti join
SET_D = { SET_A and SET_B }, ; inner join
SET_E = { SET_A or SET_B }, ; full outer join
SET_F = { not SET_A }, ; = { GLOBAL not SET_A }
SET_G = { not SET_A nor SET_B } ; = { GLOBAL not { SET_A or SET_B } }`,
tokens: [
{ type: "whiteSpace", value: "\n" },
{ type: "comment", value: "; ---- set join operations non-mutable! " },
{ type: "whiteSpace", value: "\n" },
{ type: "kwSet", value: "set" },
{ type: "whiteSpace", value: " " },
{ type: "setIdentifier", value: "SET_C" },
{ type: "whiteSpace", value: " " },
{ type: "equal", value: "=" },
{ type: "whiteSpace", value: " " },
{ type: "openCurlyBracket", value: "{" },
{ type: "whiteSpace", value: " " },
{ type: "identifier", value: "SET_A" },
{ type: "whiteSpace", value: " " },
{ type: "kwSetNot", value: "not" },
{ type: "whiteSpace", value: " " },
{ type: "identifier", value: "SET_B" },
{ type: "whiteSpace", value: " " },
{ type: "closeCurlyBracket", value: "}" },
{ type: "comma", value: "," },
{ type: "comment", value: " ; left anti join" },
{ type: "whiteSpace", value: "\n " },
{ type: "setIdentifier", value: "SET_D" },
{ type: "whiteSpace", value: " " },
{ type: "equal", value: "=" },
{ type: "whiteSpace", value: " " },
{ type: "openCurlyBracket", value: "{" },
{ type: "whiteSpace", value: " " },
{ type: "identifier", value: "SET_A" },
{ type: "whiteSpace", value: " " },
{ type: "kwSetAnd", value: "and" },
{ type: "whiteSpace", value: " " },
{ type: "identifier", value: "SET_B" },
{ type: "whiteSpace", value: " " },
{ type: "closeCurlyBracket", value: "}" },
{ type: "comma", value: "," },
{ type: "comment", value: " ; inner join" },
{ type: "whiteSpace", value: "\n " },
{ type: "setIdentifier", value: "SET_E" },
{ type: "whiteSpace", value: " " },
{ type: "equal", value: "=" },
{ type: "whiteSpace", value: " " },
{ type: "openCurlyBracket", value: "{" },
{ type: "whiteSpace", value: " " },
{ type: "identifier", value: "SET_A" },
{ type: "whiteSpace", value: " " },
{ type: "kwSetOr", value: "or" },
{ type: "whiteSpace", value: " " },
{ type: "identifier", value: "SET_B" },
{ type: "whiteSpace", value: " " },
{ type: "closeCurlyBracket", value: "}" },
{ type: "comma", value: "," },
{ type: "comment", value: " ; full outer join" },
{ type: "whiteSpace", value: "\n " },
{ type: "setIdentifier", value: "SET_F" },
{ type: "whiteSpace", value: " " },
{ type: "equal", value: "=" },
{ type: "whiteSpace", value: " " },
{ type: "openCurlyBracket", value: "{" },
{ type: "whiteSpace", value: " " },
{ type: "kwSetNot", value: "not" },
{ type: "whiteSpace", value: " " },
{ type: "identifier", value: "SET_A" },
{ type: "whiteSpace", value: " " },
{ type: "closeCurlyBracket", value: "}" },
{ type: "comma", value: "," },
{ type: "comment", value: " ; = { GLOBAL not SET_A }" },
{ type: "whiteSpace", value: "\n " },
{ type: "setIdentifier", value: "SET_G" },
{ type: "whiteSpace", value: " " },
{ type: "equal", value: "=" },
{ type: "whiteSpace", value: " " },
{ type: "openCurlyBracket", value: "{" },
{ type: "whiteSpace", value: " " },
{ type: "kwSetNot", value: "not" },
{ type: "whiteSpace", value: " " },
{ type: "identifier", value: "SET_A" },
{ type: "whiteSpace", value: " " },
{ type: "kwSetNor", value: "nor" },
{ type: "whiteSpace", value: " " },
{ type: "identifier", value: "SET_B" },
{ type: "whiteSpace", value: " " },
{ type: "closeCurlyBracket", value: "}" },
{ type: "whiteSpace", value: " " },
{ type: "comment", value: "; = { GLOBAL not { SET_A or SET_B } }" },
],
},
setOperations: {
latl: `
; ---- set character operations - non-mutable!
set SET_B = { [ Xy ] in SET_A }, ; FILTER: where X is any character and y is a filtering character
SET_C = { SET_A yield [ Xy ] }, ; CONCATENATE: performs transformation with (prepended or) appended character
SET_D = { SET_A yield [ X concat y ] },
SET_E = { SET_A yield [ y concat X ] },
SET_F = { SET_A yield y[ X ] }, ; DISSOCIATE: performs transformation removing prepended (or appended) character
SET_G = { SET_A yield y dissoc [ X ] },
SET_H = { SET_A yield [ X ] dissoc y },
SET_I = { [ Xy ] in SET_A yield [ X ]y } ; combined FILTER and DISSOCIATE`,
tokens: [
{ type: "whiteSpace", value: "\n" },
{
type: "comment",
value: "; ---- set character operations - non-mutable!",
},
{ type: "whiteSpace", value: "\n" },
{ type: "kwSet", value: "set" },
{ type: "whiteSpace", value: " " },
{ type: "setIdentifier", value: "SET_B" },
{ type: "whiteSpace", value: " " },
{ type: "equal", value: "=" },
{ type: "whiteSpace", value: " " },
{ type: "openCurlyBracket", value: "{" },
{ type: "whiteSpace", value: " " },
{ type: "openSquareBracket", value: "[" },
{ type: "whiteSpace", value: " " },
{ type: "identifier", value: "X" },
{ type: "phone", value: "y" },
{ type: "whiteSpace", value: " " },
{ type: "closeSquareBracket", value: "]" },
{ type: "whiteSpace", value: " " },
{ type: "kwSetIn", value: "in" },
{ type: "whiteSpace", value: " " },
{ type: "identifier", value: "SET_A" },
{ type: "whiteSpace", value: " " },
{ type: "closeCurlyBracket", value: "}" },
{ type: "comma", value: "," },
{
type: "comment",
value:
" ; FILTER: where X is any character and y is a filtering character",
},
{ type: "whiteSpace", value: "\n " },
{ type: "setIdentifier", value: "SET_C" },
{ type: "whiteSpace", value: " " },
{ type: "equal", value: "=" },
{ type: "whiteSpace", value: " " },
{ type: "openCurlyBracket", value: "{" },
{ type: "whiteSpace", value: " " },
{ type: "identifier", value: "SET_A" },
{ type: "whiteSpace", value: " " },
{ type: "kwSetYield", value: "yield" },
{ type: "whiteSpace", value: " " },
{ type: "openSquareBracket", value: "[" },
{ type: "whiteSpace", value: " " },
{ type: "identifier", value: "X" },
{ type: "phone", value: "y" },
{ type: "whiteSpace", value: " " },
{ type: "closeSquareBracket", value: "]" },
{ type: "whiteSpace", value: " " },
{ type: "closeCurlyBracket", value: "}" },
{ type: "comma", value: "," },
{
type: "comment",
value:
" ; CONCATENATE: performs transformation with (prepended or) appended character",
},
{ type: "whiteSpace", value: "\n " },
{ type: "setIdentifier", value: "SET_D" },
{ type: "whiteSpace", value: " " },
{ type: "equal", value: "=" },
{ type: "whiteSpace", value: " " },
{ type: "openCurlyBracket", value: "{" },
{ type: "whiteSpace", value: " " },
{ type: "identifier", value: "SET_A" },
{ type: "whiteSpace", value: " " },
{ type: "kwSetYield", value: "yield" },
{ type: "whiteSpace", value: " " },
{ type: "openSquareBracket", value: "[" },
{ type: "whiteSpace", value: " " },
{ type: "identifier", value: "X" },
{ type: "whiteSpace", value: " " },
{ type: "kwSetConcat", value: "concat" },
{ type: "whiteSpace", value: " " },
{ type: "phone", value: "y" },
{ type: "whiteSpace", value: " " },
{ type: "closeSquareBracket", value: "]" },
{ type: "whiteSpace", value: " " },
{ type: "closeCurlyBracket", value: "}" },
{ type: "comma", value: "," },
{ type: "whiteSpace", value: "\n " },
{ type: "setIdentifier", value: "SET_E" },
{ type: "whiteSpace", value: " " },
{ type: "equal", value: "=" },
{ type: "whiteSpace", value: " " },
{ type: "openCurlyBracket", value: "{" },
{ type: "whiteSpace", value: " " },
{ type: "identifier", value: "SET_A" },
{ type: "whiteSpace", value: " " },
{ type: "kwSetYield", value: "yield" },
{ type: "whiteSpace", value: " " },
{ type: "openSquareBracket", value: "[" },
{ type: "whiteSpace", value: " " },
{ type: "phone", value: "y" },
{ type: "whiteSpace", value: " " },
{ type: "kwSetConcat", value: "concat" },
{ type: "whiteSpace", value: " " },
{ type: "identifier", value: "X" },
{ type: "whiteSpace", value: " " },
{ type: "closeSquareBracket", value: "]" },
{ type: "whiteSpace", value: " " },
{ type: "closeCurlyBracket", value: "}" },
{ type: "comma", value: "," },
{ type: "whiteSpace", value: "\n " },
{ type: "setIdentifier", value: "SET_F" },
{ type: "whiteSpace", value: " " },
{ type: "equal", value: "=" },
{ type: "whiteSpace", value: " " },
{ type: "openCurlyBracket", value: "{" },
{ type: "whiteSpace", value: " " },
{ type: "identifier", value: "SET_A" },
{ type: "whiteSpace", value: " " },
{ type: "kwSetYield", value: "yield" },
{ type: "whiteSpace", value: " " },
{ type: "phone", value: "y" },
{ type: "openSquareBracket", value: "[" },
{ type: "whiteSpace", value: " " },
{ type: "identifier", value: "X" },
{ type: "whiteSpace", value: " " },
{ type: "closeSquareBracket", value: "]" },
{ type: "whiteSpace", value: " " },
{ type: "closeCurlyBracket", value: "}" },
{ type: "comma", value: "," },
{
type: "comment",
value:
" ; DISSOCIATE: performs transformation removing prepended (or appended) character",
},
{ type: "whiteSpace", value: "\n " },
{ type: "setIdentifier", value: "SET_G" },
{ type: "whiteSpace", value: " " },
{ type: "equal", value: "=" },
{ type: "whiteSpace", value: " " },
{ type: "openCurlyBracket", value: "{" },
{ type: "whiteSpace", value: " " },
{ type: "identifier", value: "SET_A" },
{ type: "whiteSpace", value: " " },
{ type: "kwSetYield", value: "yield" },
{ type: "whiteSpace", value: " " },
{ type: "phone", value: "y" },
{ type: "whiteSpace", value: " " },
{ type: "kwSetDissoc", value: "dissoc" },
{ type: "whiteSpace", value: " " },
{ type: "openSquareBracket", value: "[" },
{ type: "whiteSpace", value: " " },
{ type: "identifier", value: "X" },
{ type: "whiteSpace", value: " " },
{ type: "closeSquareBracket", value: "]" },
{ type: "whiteSpace", value: " " },
{ type: "closeCurlyBracket", value: "}" },
{ type: "comma", value: "," },
{ type: "whiteSpace", value: "\n " },
{ type: "setIdentifier", value: "SET_H" },
{ type: "whiteSpace", value: " " },
{ type: "equal", value: "=" },
{ type: "whiteSpace", value: " " },
{ type: "openCurlyBracket", value: "{" },
{ type: "whiteSpace", value: " " },
{ type: "identifier", value: "SET_A" },
{ type: "whiteSpace", value: " " },
{ type: "kwSetYield", value: "yield" },
{ type: "whiteSpace", value: " " },
{ type: "openSquareBracket", value: "[" },
{ type: "whiteSpace", value: " " },
{ type: "identifier", value: "X" },
{ type: "whiteSpace", value: " " },
{ type: "closeSquareBracket", value: "]" },
{ type: "whiteSpace", value: " " },
{ type: "kwSetDissoc", value: "dissoc" },
{ type: "whiteSpace", value: " " },
{ type: "phone", value: "y" },
{ type: "whiteSpace", value: " " },
{ type: "closeCurlyBracket", value: "}" },
{ type: "comma", value: "," },
{ type: "whiteSpace", value: "\n " },
{ type: "setIdentifier", value: "SET_I" },
{ type: "whiteSpace", value: " " },
{ type: "equal", value: "=" },
{ type: "whiteSpace", value: " " },
{ type: "openCurlyBracket", value: "{" },
{ type: "whiteSpace", value: " " },
{ type: "openSquareBracket", value: "[" },
{ type: "whiteSpace", value: " " },
{ type: "identifier", value: "X" },
{ type: "phone", value: "y" },
{ type: "whiteSpace", value: " " },
{ type: "closeSquareBracket", value: "]" },
{ type: "whiteSpace", value: " " },
{ type: "kwSetIn", value: "in" },
{ type: "whiteSpace", value: " " },
{ type: "identifier", value: "SET_A" },
{ type: "whiteSpace", value: " " },
{ type: "kwSetYield", value: "yield" },
{ type: "whiteSpace", value: " " },
{ type: "openSquareBracket", value: "[" },
{ type: "whiteSpace", value: " " },
{ type: "identifier", value: "X" },
{ type: "whiteSpace", value: " " },
{ type: "closeSquareBracket", value: "]" },
{ type: "phone", value: "y" },
{ type: "whiteSpace", value: " " },
{ type: "closeCurlyBracket", value: "}" },
{ type: "whiteSpace", value: " " },
{ type: "comment", value: "; combined FILTER and DISSOCIATE" },
],
},
};

View file

@ -0,0 +1,10 @@
import { assertionData } from './assertionData';
import { codeGenerator } from '../codeGenerator';
describe('codeGenerator', () => {
it('parses simple comment', () => {
const { latl, code } = assertionData.simpleComment;
const generatedCode = codeGenerator(latl);
expect(generatedCode).toEqual(code);
});
})

View file

@ -0,0 +1,71 @@
import { lexer } from '../lexer';
import { assertionData } from './assertionData';
describe('lexer', () => {
const getToken = obj => obj ? formatToken(obj) : null;
const formatToken = obj => ({ type: obj.type, value: obj.value });
const getStream = latl => {
lexer.reset(latl);
let token = getToken(lexer.next());
let stream = [];
do {
stream = [...stream, token]
token = getToken(lexer.next());
} while (token);
return stream;
}
it('lexes simple comment', () => {
const { latl, tokens } = assertionData.simpleComment;
const stream = getStream(latl);
expect(stream).toStrictEqual(tokens);
});
// it('lexes simple * and identifier', () => {
// lexer.reset('*proto');
// const stream = [ getToken(lexer.next()), getToken(lexer.next()) ];
// expect(stream).toStrictEqual([ { type: 'star', value: '*' }, { type: 'identifier', value: 'proto' } ]);
// })
it('lexes set and identifier', () => {
const { latl, tokens } = assertionData.simpleSetDefinition;
const stream = getStream(latl);
expect(stream).toStrictEqual(tokens);
})
it('lexes multiple set definitions with comma operator', () => {
const { latl, tokens } = assertionData.commaSetDefinition;
const stream = getStream(latl);
expect(stream).toStrictEqual(tokens);
});
it('lexes set definition with alias', () => {
const { latl, tokens } = assertionData.setAliasDefinition;
const stream = getStream(latl);
expect(stream).toStrictEqual(tokens);
});
it('lexes set definition with set join', () => {
const { latl, tokens } = assertionData.setDefinitionJoin;
const stream = getStream(latl);
expect(stream).toStrictEqual(tokens);
});
it('lexes set definition with yield operation', () => {
const { latl, tokens } = assertionData.setDefinitionYield;
const stream = getStream(latl);
expect(stream).toStrictEqual(tokens);
});
it('lexes all set join operations', () => {
const { latl, tokens } = assertionData.setOperationsJoin;
const stream = getStream(latl);
expect(stream).toStrictEqual(tokens);
});
it('lexes set filter, concat, and dissoc operations', () => {
const { latl, tokens } = assertionData.setOperations;
const stream = getStream(latl);
expect(stream).toStrictEqual(tokens);
})
})

View file

@ -0,0 +1,180 @@
import { lexer } from "../lexer";
import { parser } from "../parser";
import { assertionData } from "./assertionData";
describe("parser", () => {
it("parses simple comment", () => {
const { latl, AST } = assertionData.simpleComment;
const feedResults = parser().feed(latl).results;
expect(feedResults.length).toBe(1);
expect(feedResults[0]).toStrictEqual(AST);
});
it("parses simple set definition", () => {
const { latl, AST } = assertionData.simpleSetDefinition;
const feedResults = parser().feed(latl).results;
expect(feedResults.length).toBe(1);
expect(feedResults[0]).toStrictEqual(AST);
});
it("parses multiple set definitions with comma operator", () => {
const { latl, AST } = assertionData.commaSetDefinition;
const feedResults = parser().feed(latl).results;
expect(feedResults.length).toBe(1);
expect(feedResults[0]).toStrictEqual(AST);
});
it("lexes set definition with alias", () => {
const { latl, AST } = assertionData.setAliasDefinition;
const feedResults = parser().feed(latl).results;
expect(feedResults[0]).toStrictEqual(AST);
});
it.skip("lexes set definition with set join", () => {
const { latl, AST } = assertionData.setDefinitionJoin;
const feedResults = parser().feed(latl).results;
expect(feedResults[0]).toStrictEqual(AST);
});
it.todo(
"lexes set definition with yield operation"
// , () => {
// const { latl, tokens } = assertionData.setDefinitionYield;
// const stream = getStream(latl);
// expect(stream).toStrictEqual(tokens);
// }
);
it.todo(
"lexes all set join operations"
// , () => {
// const { latl, tokens } = assertionData.setOperationsJoin;
// const stream = getStream(latl);
// expect(stream).toStrictEqual(tokens);
// }
);
it.todo(
"lexes set filter, concat, and dissoc operations"
// , () => {
// const { latl, tokens } = assertionData.setOperations;
// const stream = getStream(latl);
// expect(stream).toStrictEqual(tokens);
// }
);
});
// {
// "set":
// [
// [
// [
// {
// "col": 5,
// "line": 2,
// "lineBreaks": 0,
// "offset": 5,
// "text": "NASAL_PULMONIC_CONSONANTS",
// "toString": [tokenToString],
// "type": "setIdentifier",
// "value": "NASAL_PULMONIC_CONSONANTS",
// },
// null,
// {
// "col": 45,
// "line": 2,
// "lineBreaks": 0,
// "offset": 45,
// "text": "=",
// "toString": [tokenToString],
// "type": "equal",
// "value": "=",
// },
// null,
// [
// [
// {
// "col": 49,
// "line": 2,
// "lineBreaks": 0,
// "offset": 49,
// "text": "m̥",
// "toString": [tokenToString],
// "type": "phone",
// "value": "m̥",
// },
// {
// "col": 91,
// "line": 2,
// "lineBreaks": 0,
// "offset": 91,
// "text": "ɴ",
// "toString": [tokenToString],
// "type": "phone",
// "value": "ɴ",
// },
// ],
// ],
// {
// "col": 94,
// "line": 2,
// "lineBreaks": 0,
// "offset": 94,
// "text": ",",
// "toString": [tokenToString],
// "type": "comma",
// "value": ",",
// },
// null,
// ],
// ],
// - "setIdentifier": "STOP_PULMONIC_CONSONANTS",
// {
// "col": 5,
// "line": 3,
// "lineBreaks": 0,
// "offset": 100,
// "text": "STOP_PULMONIC_CONSONANTS",
// "toString": [tokenToString],
// "type": "setIdentifier",
// "value": "STOP_PULMONIC_CONSONANTS",
// },
// null,
// {
// "col": 45,
// "line": 3,
// "lineBreaks": 0,
// "offset": 140,
// "text": "=",
// "toString": [tokenToString],
// "type": "equal",
// "value": "=",
// },
// null,
// [
// [
// {
// "col": 49,
// "line": 3,
// "lineBreaks": 0,
// "offset": 144,
// "text": "p",
// "toString": [tokenToString],
// "type": "phone",
// "value": "p",
// },
// {
// "col": 104,
// "line": 3,
// "lineBreaks": 0,
// "offset": 199,
// "text": "ʔ",
// "toString": [tokenToString],
// "type": "phone",
// "value": "ʔ",
// },
// ],
// ],
// ],
// "token": "kwSet",
// }