diff --git a/__pycache__/asc.cpython-34.pyc b/__pycache__/asc.cpython-34.pyc
deleted file mode 100644
index 9381ab4..0000000
Binary files a/__pycache__/asc.cpython-34.pyc and /dev/null differ
diff --git a/__pycache__/core.cpython-34.pyc b/__pycache__/core.cpython-34.pyc
deleted file mode 100644
index 65d855e..0000000
Binary files a/__pycache__/core.cpython-34.pyc and /dev/null differ
diff --git a/app.py b/app.py
deleted file mode 100755
index 4aa6af8..0000000
--- a/app.py
+++ /dev/null
@@ -1,94 +0,0 @@
-#!./.interpreter.sh
-
-import argparse
-import asc
-import cgi
-import cgitb
-from core import *
-import os
-import sys
-
-form = cgi.FieldStorage(encoding='utf-8')
-
-word = None
-w = False
-html = False
-debug = 0
-FILE_PATH = os.path.abspath(os.path.dirname(sys.argv[0]))
-
-if 'word' in form:
- sys.stdout = Reencoder(sys.stdout)
- print('Content-Type: text/html')
- print('')
- word = form['word'].value
- startd = {}
- endd = {}
- start = []
- end = []
- if 'debug' in form:
- debug = int(form['debug'].value)
- html = True
-else:
- class DefaultAppend(argparse.Action):
-
- def __call__(self, parser, namespace, values, option_string=None):
- items = argparse._copy.copy(
- argparse._ensure_value(namespace, self.dest, []))
- if values == None:
- values = ''
- items.append(values)
- setattr(namespace, self.dest, items)
-
- parser = argparse.ArgumentParser()
- parser.add_argument('--word', '-w')
- parser.add_argument('--start', '-s', action=DefaultAppend, nargs='?')
- parser.add_argument('--end', '-e', action=DefaultAppend, nargs='?')
- parser.add_argument('--debug', '-d', type=int, default=0)
- parser.add_argument('--html', '-t', action='store_true')
- args = parser.parse_args()
- word = args.word
- if word is None:
- w = True
- debug = args.debug
- if args.html:
- sys.stdout = Reencoder(sys.stdout)
- html = True
- start = args.start
- end = args.end
-
-if html:
- cgitb.enable()
-else:
- cgitb.enable(format='plain')
-
-for f in form:
- if f[0] == 's':
- startd[f] = form[f].value
- if startd[f] == ' ':
- startd[f] = ''
- elif f[0] == 'e':
- endd[f] = form[f].value
- if endd[f] == ' ':
- endd[f] = ''
-
-start = start or [v for k, v in sorted(startd.items())]
-end = end or [v for k, v in sorted(endd.items())]
-
-pairs = list(zip(start, end))
-
-while True:
- if w:
- try:
- word = input()
- except (KeyboardInterrupt, EOFError):
- break
- word, db = asc.asc(word, pairs, debug, FILE_PATH)
-
- if html:
- print('
')
- print(word)
- print(db, end='') # lint:ok
- if html:
- print('
')
- if not w:
- break
\ No newline at end of file
diff --git a/asc.py b/asc.py
deleted file mode 100755
index 2716074..0000000
--- a/asc.py
+++ /dev/null
@@ -1,45 +0,0 @@
-#!./.interpreter.sh
-
-from conlang.conlangApp import *
-from core import *
-
-ar = soundChanger.applyRules
-
-lf = lambda f, pre='.': loadFile(pre + '/files/' + f)
-
-def last(name):
- return ('.' + name).rsplit('.', 1)[0][1:]
-
-def step(start, end):
- '''\
-provides the next step between start and end. does not check if they are
-linearly connected.'''
- if start != '':
- start += '.'
- return start + end[len(start):].split('.')[0]
-
-def asc(word, pairs, debug=0, pre='.'):
- '''\
-pairs format: [['a', 'a.b'], ['b', 'b.c']]
-debug = 0: don't show anything
-debug = 1: word at end of each pair
-debug = 2: word at end of each file
-debug = 3: word at end of each rule'''
- db = ''
- for p in pairs:
- if p[1].startswith(p[0]):
- cur, end = p
- while cur != end:
- cur = step(cur, end)
- word, steps = ar(word, lf(cur, pre))
- if debug > 2:
- db += steps
- if debug > 1:
- db += cur + ': ' + word + '\n'
- else:
- raise Exception(p[1] + ' does not start with ' + p[0])
- if debug == 1:
- db += p[1] + ': ' + word + '\n'
- return word, db
-
-saj = 'prt.west.sajura'
\ No newline at end of file
diff --git a/cgiApp.py b/cgiApp.py
new file mode 100755
index 0000000..91e989e
--- /dev/null
+++ b/cgiApp.py
@@ -0,0 +1,88 @@
+#!./.interpreter.sh
+
+import argparse
+import cgi
+import cgitb
+import os
+import sys
+from conlang import soundChangeApp, workers
+
+
+word = None
+stdin = False
+html = False
+debug = 0
+FILE_PATH = os.path.abspath(os.path.dirname(sys.argv[0]))
+
+if 'REQUEST_METHOD' in os.environ:
+ # It's being run as a cgi script
+ # Encode all non-ascii characters with xml escapes
+ sys.stdout = workers.Reencoder(sys.stdout)
+ print('Content-Type: text/html')
+ print('')
+ form = cgi.FieldStorage(encoding='utf-8')
+ word = form['word'].value
+ startd = {}
+ endd = {}
+ for f in form:
+ try:
+ # just get the number from the form key
+ n = int(f.split('-')[1])
+ v = form[f].value
+ if f[0] == 's':
+ startd[n] = v if v != ' ' else ''
+ elif f[0] == 'e':
+ endd[n] = v if v != ' ' else ''
+ except IndexError:
+ # there wasn't a '-' in f, so it wasn't a start or end key
+ continue
+ start = [v for k, v in sorted(startd.items())]
+ end = [v for k, v in sorted(endd.items())]
+ if 'debug' in form:
+ debug = int(form['debug'].value)
+ html = True
+else:
+ # It's being run from the command line
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--word', '-w')
+ parser.add_argument('--start', '-s', action='append', default=[], nargs='?')
+ parser.add_argument('--end', '-e', action='append', default=[], nargs='?')
+ parser.add_argument('--debug', '-d', type=int, default=0)
+ parser.add_argument('--html', '-t', action='store_true')
+ args = parser.parse_args()
+ word = args.word
+ if word is None:
+ # take input from stdin
+ stdin = True
+ debug = args.debug
+ if args.html:
+ # encode all non-ascii characters with xml escapes
+ sys.stdout = Reencoder(sys.stdout)
+ html = True
+ start = [x if x else '' for x in args.start]
+ end = [x if x else '' for x in args.end]
+
+if html:
+ # html formatted error messages
+ cgitb.enable()
+else:
+ # plain error messages
+ cgitb.enable(format='plain')
+
+pairs = list(zip(start, end))
+
+while True:
+ if stdin:
+ try:
+ word = input()
+ except (KeyboardInterrupt, EOFError):
+ break
+ word, db = soundChangeApp.applyRuleFiles(word, pairs, debug)
+ if html:
+ print('')
+ print(word)
+ print(db, end='')
+ if html:
+ print('
')
+ if not stdin:
+ break
diff --git a/conlang/X_soundChanger.py b/conlang/X_soundChanger.py
deleted file mode 100644
index a5537f6..0000000
--- a/conlang/X_soundChanger.py
+++ /dev/null
@@ -1,133 +0,0 @@
-#! /usr/bin/env python3.3 # lint:ok
-
-import regex
-from . import workers
-
-# rules are stored as dicts:
-# {'from': '', 'to': '', 'before': '', 'after': '',
-# 'unbefore': '', 'unafter': ''}
-
-# all cats would be included in a dict with keys being the cat name,
-# and the values being arrays of the members of that cat
-# e.g. {'nasal':['m', 'n', 'ŋ'], 'vlplos':['p', 't', 'k'], ...}
-
-
-def parseSoundChange(l, cats):
- out = {}
- if len(l.split(' = ')) == 2:
- out['catName'] = l.split(' = ')[0]
- category = l.split(' = ')[1]
- for c in cats:
- category = category.replace('{' + c + '}', ' '.join(cats[c]))
- out['category'] = category.split(' ')
- else:
- out['from'] = l.split(' > ')[0]
- if out['from'] == '0':
- out['from'] = ''
- out['from'] = out['from'].replace('#', '\\b')
- out['to'] = l.split(' > ')[1].split(' / ')[0].split(' ! ')[0]
- out['to'].replace('#', '\\b')
- if out['to'] == '0':
- out['to'] = ''
- try:
- out['before'] = l.split(' / ')[1].split('_')[0]
- out['before'] = out['before'].replace('#', '\\b')
- out['after'] = l.split(' / ')[1].split('_')[1].split(' ! ')[0]
- out['after'] = out['after'].replace('#', '\\b')
- except IndexError:
- pass
- try:
- out['unbefore'] = l.split(' ! ')[1].split('_')[0]
- out['unbefore'] = out['unbefore'].replace('#', '\\b')
- out['unafter'] = l.split(' ! ')[1].split('_')[1]
- out['unafter'] = out['unafter'].replace('#', '\\b')
- except IndexError:
- pass
- return out
-
-
-def applyRule(word, rule, cats):
- """Applies a specified rule to the given word,\
- making category replacements if necessary"""
- # create match, ignoring numbered categories
- match = '('
- if 'before' in rule and rule['before'] != '':
- match += '(?<=(?P' + rule['before'] + '))'
- if 'unbefore' in rule and rule['unbefore'] != '':
- match += '(?' + rule['from'] + ')'
- if 'after' in rule and rule['after'] != '':
- match += '(?=(?P' + rule['after'] + '))'
- if 'unafter' in rule and rule['unafter'] != '':
- match += '(?!' + rule['unafter'] + ')'
- match += ')'
- ruleStr = match
- # replace categories
- nc = [0]
-
- def catReplace(m):
- if m.group(2) in cats:
- out = '('
- if m.group(1) == '':
- out += '|'.join(cats[m.group(2)])
- else:
- out += '?P' + '|'.join(cats[m.group(2)])
- nc[0] += 1
- return out + ')'
- else:
- return m.group(0)
-
- match = regex.sub('\\{(\\d*):?([^}]*)}', catReplace, match)
-
- def fromToTo(m):
- # now u test the match to see if it matches with numbered categories
- # matched = m.group(1)
- catIndex = []
- if nc[0]:
- iNc = 0
- for c in regex.finditer('\\{(\\d+):([^}]*)}', ruleStr):
- # now what? maybe i should have it search the matched
- # string for this category, find which it is, and determine
- # which index for which numbered category
- cNum = int(c.group(1))
- cCat = c.group(2)
- cNc = 'nc' + str(iNc)
- cMatch = m.group(cNc)
- if cCat in cats:
- if len(catIndex) < cNum and catIndex[cNum] is not None:
- if cats[cCat][catIndex[cNum]] != cMatch:
- return m.group(0)
- # if anything has the wrong number,
- # quit out of this match
- else:
- workers.addPad(catIndex, cNum,
- cats[cCat].index(cMatch))
- iNc += 1
- # now, replace from (m.group('from')) with to
- toStr = rule['to']
-
- def toCatReplace(c):
- cNum = int(c.group(1))
- cCat = c.group(2)
- if cCat in cats:
- if len(catIndex) > cNum and catIndex[cNum] is not None:
- return cats[cCat][catIndex[cNum]]
-
- return regex.sub('\\{(\\d+):([^}]*)}', toCatReplace, toStr)
-
- word = regex.sub(match, fromToTo, word)
- return word
-
-
-def applyRules(word, soundChanges, showSteps=False):
- cats = {}
- for l in soundChanges:
- rc = parseSoundChange(l, cats)
- if 'catName' in rc:
- cats[rc['catName']] = rc['category']
- else:
- word = applyRule(word, rc, cats)
- if showSteps:
- print(rc, word)
- return word
\ No newline at end of file
diff --git a/conlang/__init__.py b/conlang/__init__.py
deleted file mode 100644
index ef35eb2..0000000
--- a/conlang/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-#! /usr/bin/env python3.3 # lint:ok
diff --git a/conlang/__init__.pyc b/conlang/__init__.pyc
deleted file mode 100644
index a5545a8..0000000
Binary files a/conlang/__init__.pyc and /dev/null differ
diff --git a/conlang/__pycache__/__init__.cpython-32.pyc b/conlang/__pycache__/__init__.cpython-32.pyc
deleted file mode 100644
index e9da748..0000000
Binary files a/conlang/__pycache__/__init__.cpython-32.pyc and /dev/null differ
diff --git a/conlang/__pycache__/__init__.cpython-33.pyc b/conlang/__pycache__/__init__.cpython-33.pyc
deleted file mode 100644
index 415d5be..0000000
Binary files a/conlang/__pycache__/__init__.cpython-33.pyc and /dev/null differ
diff --git a/conlang/__pycache__/__init__.cpython-34.pyc b/conlang/__pycache__/__init__.cpython-34.pyc
deleted file mode 100644
index 0d3ec3b..0000000
Binary files a/conlang/__pycache__/__init__.cpython-34.pyc and /dev/null differ
diff --git a/conlang/__pycache__/conlangApp.cpython-32.pyc b/conlang/__pycache__/conlangApp.cpython-32.pyc
deleted file mode 100644
index d878346..0000000
Binary files a/conlang/__pycache__/conlangApp.cpython-32.pyc and /dev/null differ
diff --git a/conlang/__pycache__/conlangApp.cpython-33.pyc b/conlang/__pycache__/conlangApp.cpython-33.pyc
deleted file mode 100644
index 0d346c3..0000000
Binary files a/conlang/__pycache__/conlangApp.cpython-33.pyc and /dev/null differ
diff --git a/conlang/__pycache__/conlangApp.cpython-34.pyc b/conlang/__pycache__/conlangApp.cpython-34.pyc
deleted file mode 100644
index 93fcdd9..0000000
Binary files a/conlang/__pycache__/conlangApp.cpython-34.pyc and /dev/null differ
diff --git a/conlang/__pycache__/dictionary.cpython-33.pyc b/conlang/__pycache__/dictionary.cpython-33.pyc
deleted file mode 100644
index 3ccc44d..0000000
Binary files a/conlang/__pycache__/dictionary.cpython-33.pyc and /dev/null differ
diff --git a/conlang/__pycache__/dictionary.cpython-34.pyc b/conlang/__pycache__/dictionary.cpython-34.pyc
deleted file mode 100644
index 8e465d1..0000000
Binary files a/conlang/__pycache__/dictionary.cpython-34.pyc and /dev/null differ
diff --git a/conlang/__pycache__/inflections.cpython-33.pyc b/conlang/__pycache__/inflections.cpython-33.pyc
deleted file mode 100644
index 572825e..0000000
Binary files a/conlang/__pycache__/inflections.cpython-33.pyc and /dev/null differ
diff --git a/conlang/__pycache__/inflections.cpython-34.pyc b/conlang/__pycache__/inflections.cpython-34.pyc
deleted file mode 100644
index 1f5c777..0000000
Binary files a/conlang/__pycache__/inflections.cpython-34.pyc and /dev/null differ
diff --git a/conlang/__pycache__/soundChanger.cpython-32.pyc b/conlang/__pycache__/soundChanger.cpython-32.pyc
deleted file mode 100644
index 9294f14..0000000
Binary files a/conlang/__pycache__/soundChanger.cpython-32.pyc and /dev/null differ
diff --git a/conlang/__pycache__/soundChanger.cpython-33.pyc b/conlang/__pycache__/soundChanger.cpython-33.pyc
deleted file mode 100644
index ba12f88..0000000
Binary files a/conlang/__pycache__/soundChanger.cpython-33.pyc and /dev/null differ
diff --git a/conlang/__pycache__/soundChanger.cpython-34 (department-of-alchemy's conflicted copy 2014-07-23).pyc b/conlang/__pycache__/soundChanger.cpython-34 (department-of-alchemy's conflicted copy 2014-07-23).pyc
deleted file mode 100644
index b9baf7d..0000000
Binary files a/conlang/__pycache__/soundChanger.cpython-34 (department-of-alchemy's conflicted copy 2014-07-23).pyc and /dev/null differ
diff --git a/conlang/__pycache__/soundChanger.cpython-34.pyc b/conlang/__pycache__/soundChanger.cpython-34.pyc
deleted file mode 100644
index d7389ca..0000000
Binary files a/conlang/__pycache__/soundChanger.cpython-34.pyc and /dev/null differ
diff --git a/conlang/__pycache__/soundChanger2.cpython-33.pyc b/conlang/__pycache__/soundChanger2.cpython-33.pyc
deleted file mode 100644
index 27a1716..0000000
Binary files a/conlang/__pycache__/soundChanger2.cpython-33.pyc and /dev/null differ
diff --git a/conlang/__pycache__/workers.cpython-32.pyc b/conlang/__pycache__/workers.cpython-32.pyc
deleted file mode 100644
index 395f66d..0000000
Binary files a/conlang/__pycache__/workers.cpython-32.pyc and /dev/null differ
diff --git a/conlang/__pycache__/workers.cpython-33.pyc b/conlang/__pycache__/workers.cpython-33.pyc
deleted file mode 100644
index fd070fb..0000000
Binary files a/conlang/__pycache__/workers.cpython-33.pyc and /dev/null differ
diff --git a/conlang/__pycache__/workers.cpython-34.pyc b/conlang/__pycache__/workers.cpython-34.pyc
deleted file mode 100644
index 53708b7..0000000
Binary files a/conlang/__pycache__/workers.cpython-34.pyc and /dev/null differ
diff --git a/conlang/conlangApp.py b/conlang/conlangApp.py
deleted file mode 100644
index 0c7947b..0000000
--- a/conlang/conlangApp.py
+++ /dev/null
@@ -1,87 +0,0 @@
-#! /usr/bin/env python3.3 # lint:ok
-
-import json
-from . import soundChanger, dictionary, inflections
-
-
-def customEncode(obj):
- '''Custom JSON encoder for Dictionary and Entry classes'''
- if isinstance(obj, dictionary.Dictionary):
- key = '__{!s}__'.format(obj.__class__.__name__)
- return {key: list(obj)}
- elif isinstance(obj, dictionary.Entry):
- key = '__{!s}__'.format(obj.__class__.__name__)
- return {key: obj.items()}
- else:
- raise TypeError("obj {} of type {}".format(obj, type(obj)))
-
-
-def classHook(dct):
- """JSON object hook to decode classes"""
- if len(dct) == 1:
- className, value = next(iter(dct.items()))
- className = className.strip('_')
- if className == 'Dictionary':
- return dictionary.Dictionary(value)
- elif className == 'Entry':
- return dictionary.Entry(value)
- return dct
-
-
-def loadFile(fileName):
- '''Returns specified file as a Dictionary or Entry if it is a json file,
- or a list of lines otherwise'''
- with open(fileName, encoding='utf-8') as f:
- if fileName.split('.')[-1] in ['json', 'js']:
- return json.load(f, object_hook=classHook)
- else:
- return [l.strip('\n') for l in f if len(l.strip('\n')) and l[:1] != '//']
-
-
-def saveFile(d, fileName, override=False):
- '''Saves the specified Dictionary to the specifed filename'''
- try:
- f = open(fileName, 'x')
- except FileExistsError:
- if not override:
- print('File exists, overwrite? [Y/n]', end=' ') # lint:ok
- if 'n' in input():
- return
- f = open(fileName, 'w')
- finally:
- json.dump(d, f, default=customEncode)
- f.close()
-
-
-def addEntry(d, fields=('word', 'pron', 'pos', 'cl', 'subcl', 'de')):
- '''Prompts user to enter specified fields, or, if none are specified,
- word, pron, pos, cl, subcl, de'''
- e = dictionary.Entry()
- for f in fields:
- print(f, end=': ')
- e[f] = input()
- if e[f] == '':
- del e[f]
- d.append(dictionary.Entry(e))
-
-
-def applyRules(d, rc, field='pron', field2=None):
- '''\
-Returns a copy of d with field2 replaced by field with the list of rules
-applied. field defaults to pron, and field2 defaults to whatever field is'''
- out = dictionary.Dictionary()
- for e in d:
- o = dictionary.Entry(e)
- if field2 is None:
- field2 = field
- o[field2] = soundChanger.applyRules(o[field], rc)[0]
- out.append(o)
- return out
-
-
-def sortFields(d, fields):
- for i in range(len(d)):
- e = dictionary.Entry()
- for f in fields:
- e[f] = d[i][f]
- d[i] = e
diff --git a/conlang/conlangApp.pyc b/conlang/conlangApp.pyc
deleted file mode 100644
index 47a611c..0000000
Binary files a/conlang/conlangApp.pyc and /dev/null differ
diff --git a/conlang/conlangDictionary.nja b/conlang/conlangDictionary.nja
deleted file mode 100644
index 580d82e..0000000
--- a/conlang/conlangDictionary.nja
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "venv": "",
- "project-type": "Import from sources",
- "name": "conlangDictionary",
- "license": "GNU General Public License v3",
- "description": ""
-}
\ No newline at end of file
diff --git a/conlang/dictionary.py b/conlang/dictionary.py
index 388df5f..f6ddbde 100644
--- a/conlang/dictionary.py
+++ b/conlang/dictionary.py
@@ -1,28 +1,69 @@
-#! /usr/bin/env python3.3 # lint:ok
-
import regex
import collections
-from . import soundChanger
+import os
+from . import entryFormat, soundChanger, soundChangeApp
+
+
+def customEncode(obj):
+ """Custom JSON encoder for Dictionary and Entry classes"""
+ if isinstance(obj, Dictionary):
+ key = '__{!s}__'.format(obj.__class__.__name__)
+ return {key: [list(obj), obj.alpha, obj.pat, obj.patArgs]}
+ elif isinstance(obj, Entry):
+ key = '__{!s}__'.format(obj.__class__.__name__)
+ return {key: [obj.items(), obj.pat, obj.patArgs]}
+ else:
+ raise TypeError("obj {} of type {}".format(obj, type(obj)))
+
+
+def classHook(dct):
+ """JSON object hook to decode classes"""
+ if len(dct) == 1:
+ className, value = next(iter(dct.items()))
+ className = className.strip('_')
+ if className == 'Dictionary':
+ return Dictionary(*value)
+ elif className == 'Entry':
+ return Entry(*value)
+ return dct
class Dictionary(collections.UserList):
"""A dictionary containing entries in a conlang"""
+ def __init__(self, l=[], alpha=None, pat=None, patArgs={}):
+ self.alpha = alpha
+ self.pat = pat
+ self.patArgs = patArgs
+ super().__init__()
+ for e in l:
+ self.append(Entry(e, pat, patArgs))
+
+ @classmethod
+ def fromJSON(cls, filename):
+ """Loads a Dictionary from a JSON file."""
+ with open(os.path.expanduser(filename), encoding='utf-8') as f:
+ return json.load(f, object_hook=classHook)
+
def __add__(self, d):
- return Dictionary(super().__add__(d))
+ return Dictionary(super().__add__(d), self.alpha, self.pat,
+ self.patArgs)
def __getitem__(self, i):
if isinstance(i, slice):
- return Dictionary(super().__getitem__(i))
+ return Dictionary(super().__getitem__(i),
+ self.alpha, self.pat, self.patArgs)
elif isinstance(i, str):
return StringList(self, i)
elif isinstance(i, tuple):
- return Dictionary([e[i] for e in self])
+ return Dictionary([e[i] for e in self], self.alpha, self.pat,
+ self.patArgs)
else:
return super().__getitem__(i)
def __mul__(self, n):
- return Dictionary(super().__mul__(n))
+ return Dictionary(super().__mul__(n), self.alpha, self.pat,
+ self.patArgs)
def __setitem__(self, index, data):
if isinstance(index, str):
@@ -39,37 +80,114 @@ def __setitem__(self, index, data):
super().__setitem__(index, data)
def __str__(self):
- return '\n'.join([str(e) for e in self])
+ return self.formatString()
+
+ def applyRuleList(self, lines, field1='pron', field2=None):
+ """Applies the list of rules specified by lines to field1 in each entry
+ of the dictionary, and sets field2 of each entry to the result. field1
+ defaults to 'pron', and field2 defaults to whatever field1 is."""
+ if field2 is None:
+ field2 = field1
+ for e in self:
+ # soundChangeApp.applyRuleList returns a tuple of the word and the
+ # debug lines, but we only want the word
+ e[field2] = soundChangeApp.applyRuleList(e[field1], lines)[0]
+
+ def applyRuleFiles(self, pairs, field1='pron', field2=None):
+ """Applies the specified sound change files specified by pairs (as in
+ soundChangeApp.applyRuleFiles) to field1 of each entry, setting field2
+ the entry to the result. field1 defaults to 'pron', and field2 defaults
+ to whatever field1 is."""
+ if field2 is None:
+ field2 = field1
+ for e in self:
+ # soundChangeApp.applyRuleFiles returns a tuple of the word and the
+ # debug lines, but we only want the word
+ e[field2] = soundChangeApp.applyRuleFiles(e[field1], pairs)[0]
def delete(self, entry):
"""Removes an entry from the dictionary"""
del self[self.index(entry)]
- def formatString(self, fFunct=None):
- if fFunct is None:
- return str(self)
- return '\n'.join([e.formatString(fFunct) for e in self])
+ def formatString(self, pat=None, patArgs={}):
+ if pat is None:
+ pat = self.pat
+ if patArgs == {}:
+ patArgs = self.patArgs
+ return '\n'.join(e.formatString(pat, patArgs) for e in self)
+
+ def orderFields(self, fields):
+ """Orders the fields of each entry according to fields. If an entry
+ lacks one of the fields, it is set to a value of ''."""
+ for i in range(len(self)):
+ e = Entry()
+ for f in fields:
+ e[f] = self[i].get(f, '')
+ self[i] = e
def search(self, s, field='word', cats=None):
- """Searches the dictionary for the specified string in the specified\
- field and returns all matches. field defaults to 'word'"""
- out = Dictionary()
+ """Searches the dictionary for the specified string in the specified
+ field and returns all matches. field defaults to 'word'"""
+ out = Dictionary(alpha=self.alpha, pat=self.pat, patArgs=self.patArgs)
for e in self:
if e.check(s, field, cats):
out.append(e)
return out
def sort(self, field='word', order=None):
- """Returns the dictionary's entries sorted on the specified field,\
- by the ordering function passed, or default string ordering if none is\
+ """Returns the dictionary's entries sorted on the specified field, by
+ the ordering function passed, or default string ordering if none is
passed. field defaults to 'word'"""
- return Dictionary(sorted(self, key=lambda x: x.orderKey(field, order)))
+ if field == 'word' and order is None:
+ order = self.alpha
+ return Dictionary(sorted(self, key=lambda x: x.orderKey(field, order)),
+ self.alpha, self.pat, self.patArgs)
+
+ def toJSON(self, filename, override=False):
+ """Saves the dictionary to the specified file"""
+ try:
+ f = open(os.path.expanduser(filename), 'x', encoding='utf-8')
+ except FileExistsError:
+ if not override:
+ print('File exists, overwrite? [Y/n]', end=' ')
+ if 'n' in input().lower():
+ return
+ f = open(os.path.expanduser(fileName), 'w', encoding='utf-8')
+ finally:
+ json.dump(self, f, default=customEncode)
+ f.close()
class Entry(collections.UserList):
"""A dictionary entry"""
- def __init__(self, e=[]):
+ def __init__(self, e=[], pat=None, patArgs={}):
+ if pat is None:
+ self.pat = r'$word$pron$pos$cl$de'
+ if patArgs == {}:
+ self.patArgs = {}
+ self.patArgs['pron'] = ' /$pron/'
+ self.patArgs['pos'] = ' - $pos'
+ self.patArgs['cl'] = ' ($cl$subcl)'
+ self.patArgs['subcl'] = '.$subcl'
+ self.patArgs['de'] = ': $de'
+ else:
+ self.patArgs = patArgs
+ else:
+ self.pat = pat
+ self.patArgs = patArgs
+ try:
+ matcher = entryFormat.match(self.pat, self.patArgs)
+ m = matcher.match(e)
+ if m is not None:
+ e = m.groupdict()
+ for f in self.patArgs:
+ if '?P<' + f + '>' not in matcher.pattern:
+ e[f] = self.patArgs[f]
+ else:
+ e = {}
+ except TypeError:
+ pass
try:
super().__init__(e.items())
except AttributeError:
@@ -78,9 +196,9 @@ def __init__(self, e=[]):
def __add__(self, e):
try:
- return Entry(super().__add__(e.items()))
+ return Entry(super().__add__(e.items()), self.pat, self.patArgs)
except AttributeError:
- return Entry(super().__add__(e))
+ return Entry(super().__add__(e), self.pat, self.patArgs)
def __contains__(self, i):
return i in self.lookup
@@ -95,20 +213,21 @@ def __delitem__(self, i):
def __getitem__(self, i):
try:
if isinstance(i, slice):
- return Entry(super().__getitem__(i))
+ return Entry(super().__getitem__(i), self.pat, self.patArgs)
else:
return super().__getitem__(i)
except TypeError:
if isinstance(i, str):
return self.lookup[i]
else:
- return Entry([(f, self.lookup[f]) for f in i if f in self])
+ return Entry([(f, self.lookup[f]) for f in i if f in self],
+ self.pat, self.patArgs)
def __iter__(self):
return iter(self.keys())
def __repr__(self):
- return repr(self.lookup)
+ return '{' + ', '.join(repr(k) + ': ' + repr(v) for k, v in l) + '}'
def __setitem__(self, i, v):
try:
@@ -122,40 +241,33 @@ def __setitem__(self, i, v):
self.lookup[i] = v
def __str__(self):
- out = ''
- if 'word' in self.lookup:
- out += self.lookup['word']
- if 'pron' in self.lookup and self.lookup['pron']:
- out += '(' + self.lookup['pron'] + ')'
- if 'pos' in self.lookup and self.lookup['pos']:
- out += ' - ' + self.lookup['pos']
- if 'cl' in self.lookup and self.lookup['cl']:
- out += '(' + self.lookup['cl']
- if 'subcl' in self.lookup and self.lookup['subcl']:
- out += '.' + self.lookup['subcl']
- out += ')'
- if 'de' in self.lookup and self.lookup['de']:
- out += ': ' + self.lookup['de']
- return out
+ return self.formatString()
def check(self, s, field, cats=None):
- """Checks to see if the entry contains the specified string\
- in the specified field"""
+ """Checks to see if the entry contains the specified string in the
+ specified field"""
if cats is None:
- return (field in self.lookup) and (s in self.lookup[field])
+ if field in self.lookup and self.lookup[field] is not None:
+ return regex.search(s, self.lookup[field]) is not None
+ else:
+ return False
else:
try:
try:
- m = soundChanger.findMatch(self.lookup[field], s, cats)
+ m = soundChanger.findMatches(self.lookup[field], s, cats)
except TypeError:
- s = soundChanger.parseSoundChange(s, cats)
- m = soundChanger.findMatch(self.lookup[field], s, cats)
+ s = soundChangeApp.parseRule(s, cats)
+ m = soundChanger.findMatches(self.lookup[field], s, cats)
return m[0] is not None
except KeyError:
return False
- def formatString(self, fFunct=__str__):
- return fFunct(self)
+ def formatString(self, pat=None, patArgs={}):
+ if pat is None:
+ pat = self.pat
+ if patArgs == {}:
+ patArgs = self.patArgs
+ return entryFormat.output(self, pat, patArgs)
def get(self, key, default=None):
return self.lookup.get(key, default)
@@ -167,17 +279,20 @@ def keys(self):
return [k for k, v in self.data]
def orderKey(self, field, order=None):
- """Returns a key based on the ordering function passed,\
- or based on default string ordering if none is passed"""
+ """Returns a key based on the ordering function passed, or based on
+ default string ordering if none is passed"""
try:
return order(self.lookup[field])
except TypeError:
- return self.lookup[field]
+ try:
+ return sortKey(order)(self.lookup[field])
+ except AttributeError:
+ return self.lookup[field]
def reorderFields(self, fields):
"""Returns an Entry with the fields reordered in the order specified by
fields"""
- e = Entry()
+ e = Entry(pat=self.pat, patArgs=self.patArgs)
for f in fields:
e[f] = self.get(f, '')
return e
@@ -207,15 +322,20 @@ def __setitem__(self, i, data):
def sortKey(alpha):
- """Returns a key for sorting in the alphabetical order in list alpha"""
- a = sorted(alpha, key=lambda x: len(x))
+ """Returns a key for sorting in the alphabetical order in dict alpha, which
+ has the format {'a': 1, 'b': 2 ...}. If a character or sequence of
+ characters is to be ignored in alphabetization, it should correspond to a
+ value of None in this dict. Characters not in this dict will be sorted at
+ position -1"""
+ a = sorted(alpha.keys(), key=lambda x: -len(x))
def key(word):
out = []
- for m in regex.finditer('|'.join(a) + '.', word):
- try:
- out.append(alpha.index(m[0]))
- except ValueError:
+ for m in regex.finditer('(' + '|'.join(a) + ')|.', word):
+ if m.group(1):
+ if alpha[m[0]] is not None:
+ out.append(alpha[m[0]])
+ else:
out.append(-1)
return out
diff --git a/conlang/entryFormat.py b/conlang/entryFormat.py
new file mode 100644
index 0000000..8f2824e
--- /dev/null
+++ b/conlang/entryFormat.py
@@ -0,0 +1,55 @@
+import regex
+from .workers import sliceReplace
+
+varMatcher = lambda s: r'(?.*?)'
+
+def match(pat, patArgs):
+ """In pat, '$foo' expands recursively to '(' + patArgs['foo'] ')?', if
+ 'foo' is in patArgs, or '(?P.*?)' otherwise. In patArgs['foo'], '$foo'
+ expands to '(?P.*?)'. '^' and '$' are circumpended. ' ' expands to
+ '\\s+'."""
+ args = {}
+ for f in patArgs:
+ args[f] = regex.escape(patArgs[f], True).replace(r'\$', '$')
+ m = regex.search(varMatcher(f), args[f])
+ while m is not None:
+ sp = (m.start(1) - 1, m.end(1))
+ args[f] = sliceReplace(args[f], sp, varGroup(f))
+ m = regex.search(varMatcher(f), args[f])
+ pat = '^' + regex.escape(pat, True).replace(r'\$', '$') + '$'
+ m = varMatch.search(pat)
+ while m is not None:
+ sp = (m.start(1) -1, m.end(1))
+ f = m.group(1)
+ pat = sliceReplace(pat, sp, '(' + args.get(f, varGroup(f)) + ')?')
+ m = varMatch.search(pat)
+ pat = pat.replace(' ', r'\s+')
+ return regex.compile(pat)
+
+def output(entry, pat, patArgs):
+ """In pat, if 'foo' is in entry, '$foo' expands recursively to
+ patArgs['foo'] if 'foo' is in patArgs, or entry['foo'] otherwise. In
+ patArgs['foo'], '$foo' expands to entry['foo']."""
+ args = {}
+ for f in patArgs:
+ args[f] = patArgs[f]
+ m = regex.search(varMatcher(f), args[f])
+ while m is not None:
+ sp = (m.start(1) - 1, m.end(1))
+ args[f] = sliceReplace(args[f], sp, entry.get(f) or '')
+ m = regex.search(varMatcher(f), args[f])
+ out = pat
+ m = varMatch.search(out)
+ while m is not None:
+ sp = (m.start(1) -1, m.end(1))
+ f = m.group(1)
+ if f in entry and entry[f] is not None:
+ out = sliceReplace(out, sp, args.get(f, entry[f]))
+ else:
+ out = sliceReplace(out, sp, '')
+ m = varMatch.search(out)
+ return out
+
+
diff --git a/conlang/features.txt b/conlang/features.txt
deleted file mode 100644
index 1d32101..0000000
--- a/conlang/features.txt
+++ /dev/null
@@ -1,8 +0,0 @@
-Features:
-+ Searchable
-+ Customizable categories for Entry data
-+ Sortable by custom order
-- Link words to translations (huh?)
-+ Automatic generation of pronunciation from orthography (via sca)
-+ Automatic generation of orthography from pronunciation (via sca)
-+ Sound change applier
diff --git a/conlang/inflections.py b/conlang/inflections.py
deleted file mode 100644
index 2387cf0..0000000
--- a/conlang/inflections.py
+++ /dev/null
@@ -1,32 +0,0 @@
-#! /usr/bin/env python3.3 # lint:ok
-
-
-class InflectionTable():
-
- def __init__(self, rows):
- self.rows = rows
- self.data = {}
- numCells = 0
- for r in rows:
- numCells += len(r)
- for i in range(numCells):
- cell = ()
- for r in rows:
- cell += (r[i % len(r)],)
- i //= len(r)
- self.data[cell] = None
-
- def parse(self, string):
- return tuple(string.split('.'))
-
- def setCell(self, rc, *cell):
- if tuple(cell) in self.data:
- self.data[tuple(cell)] = rc
- else:
- self.data[self.parse(cell[0])] = rc
-
- def getCell(self, *cell):
- if tuple(cell) in self.data:
- return self.data[tuple(cell)]
- else:
- return self.data[self.parse(cell[0])]
\ No newline at end of file
diff --git a/conlang/out.js b/conlang/out.js
deleted file mode 100644
index c612007..0000000
--- a/conlang/out.js
+++ /dev/null
@@ -1 +0,0 @@
-{"className": "Dictionary", "entries": [{"className": "Entry", "fields": {"en": "cat", "word": "cada", "cl": "a", "pos": "noun", "pron": "ka\u00f0\u0259", "subcl": ""}}, {"className": "Entry", "fields": {"en": "rock", "word": "d\u00eda", "cl": "i", "pos": "noun", "pron": "\u00f0i\u0259", "subcl": ""}}]}
\ No newline at end of file
diff --git a/conlang/out.json b/conlang/out.json
deleted file mode 100644
index 2d1a223..0000000
--- a/conlang/out.json
+++ /dev/null
@@ -1 +0,0 @@
-{"entries": [{"fields": {"pron": "ka\u00f0\u0259", "pos": "noun", "cl": "a", "word": "cada", "subcl": "", "en": "cat"}, "className": "Entry"}, {"fields": {"pron": "\u00f0i\u0259", "pos": "noun", "cl": "i", "word": "d\u00eda", "subcl": "", "en": "rock"}, "className": "Entry"}], "className": "Dictionary"}
\ No newline at end of file
diff --git a/conlang/soundChangeApp.py b/conlang/soundChangeApp.py
new file mode 100644
index 0000000..72038dd
--- /dev/null
+++ b/conlang/soundChangeApp.py
@@ -0,0 +1,132 @@
+import os
+from . import soundChanger, workers
+
+def parseRule(l, cats):
+ """Given a line l with the format 'a > b / c_d ! e_f' produces a dict of
+ the form {'from': 'a', 'to': 'b', 'before': 'c', 'after': 'd', 'unbefore':
+ 'e', 'unafter': 'f'}. If l is of the form 'a = b c d', the output is a dict
+ of the form {'catName': 'a', 'category': ['b', 'c', 'd']}. Category names
+ in curly brackets are expanded."""
+ wordBoundary = r'((?<=^|\s)|(?=$|\s))'
+ out = {}
+ if len(l.split(' = ')) == 2:
+ # If there is an equals sign, it's a category
+ out['catName'] = l.split(' = ')[0].strip()
+ category = l.split(' = ')[1]
+ # expand categories
+ for c in cats:
+ category = category.replace('{' + c + '}', ' '.join(cats[c]))
+ out['category'] = category.split()
+ else:
+ # Otherwise, it's a sound change rule
+ try:
+ # Attempt to set 'from' and 'to'. If there isn't a ' > ', it will
+ # raise an IndexError when trying to set 'to', so 'from' will be
+ # set, but 'to' will not. This could be used when parsing a rule to
+ # be used as a search pattern, and not as a sound change. Need to
+ # split on ' / ' and ' ! ' in case it is being used in this way.
+ out['from'] = l.split(' > ')[0].split(' / ')[0].split(' ! ')[0]
+ # Treat '0' like ''
+ if out['from'] == '0':
+ out['from'] = ''
+ out['from'] = out['from'].replace('#', wordBoundary)
+ out['to'] = l.split(' > ')[1].split(' / ')[0].split(' ! ')[0]
+ out['to'].replace('#', wordBoundary)
+ # Treat '0' like ''
+ if out['to'] == '0':
+ out['to'] = ''
+ except IndexError:
+ pass
+ try:
+ # Attempt to set 'before' and 'after'. If there isn't a ' / ', it
+ # will raise an IndexError, and neither will be set. If there isn't
+ # a '_', it will raise an IndexError when trying to set 'after', so
+ # 'before' will be set, but 'after' will not.
+ out['before'] = l.split(' / ')[1].split('_')[0].split(' ! ')[0]
+ out['before'] = out['before'].replace('#', wordBoundary)
+ out['after'] = l.split(' / ')[1].split('_')[1].split(' ! ')[0]
+ out['after'] = out['after'].replace('#', wordBoundary)
+ except IndexError:
+ pass
+ try:
+ # Attempt to set 'unbefore' and 'unafter'. Same comments apply as
+ # for 'before' and 'after'. Note that the negative conditions must
+ # come after the positive conditions, if both exist.
+ out['unbefore'] = l.split(' ! ')[1].split('_')[0]
+ out['unbefore'] = out['unbefore'].replace('#', wordBoundary)
+ out['unafter'] = l.split(' ! ')[1].split('_')[1]
+ out['unafter'] = out['unafter'].replace('#', wordBoundary)
+ except IndexError:
+ pass
+ return out
+
+
+def applyRuleList(word, lines):
+ """Applies the list of rules specified by lines to word. Returns a tuple of
+ the final word and debug, which lists the outcome of each rule."""
+ cats = {}
+ debug = ''
+ for l in lines:
+ rc = parseRule(l, cats)
+ if 'catName' in rc:
+ cats[rc['catName']] = rc['category']
+ debug += l + '\n'
+ else:
+ word = soundChanger.applyRule(word, rc, cats)
+ debug += l + ' ' + word
+ return word, debug
+
+
+def loadTextFile(filename):
+ """Loads a text file as a list of lines, ignoring blank lines and lines
+ starting with '//'."""
+ with open(os.path.expanduser(filename), encoding='utf-8') as f:
+ return [l.strip('\n') for l in f if l.strip() and l[:1] != '//']
+
+
+lf = lambda f: loadTextFile(workers.FILE_PATH + '/files/' + f)
+
+
+def step(start, end):
+ """Provides the next step between start and end. Does not check if they are
+ linearly connected."""
+ if start != '':
+ # add a dot after nonempty strings before adding the next step. the dot
+ # can't come from end, because we get the next step using .split('.'),
+ # which necessarily will not contain a '.'
+ start += '.'
+ return start + end[len(start):].split('.')[0]
+
+
+def applyRuleFiles(word, pairs, debug=0):
+ """pairs format: (('a', 'a.b.c'), ('c', 'c.d')) or (('a', '.b.c'), ('c',
+ '.d')), where FILE_PATH/files/a.b, FILE_PATH/files/a.b.c, and
+ FILE_PATH/files/c.d each contain a list of sound change rules.
+ debug = 0: don't show anything
+ debug = 1: word at end of each pair
+ debug = 2: word at end of each file
+ debug = 3: word at end of each rule
+ Returns a tuple of the final word and the debug info."""
+ # if any debug info is to be shown, start by adding the initial word
+ if len(pairs) > 0:
+ db = pairs[0][0] + ': ' + word + '\n' if debug else ''
+ for p in pairs:
+ if len(p[1]) > 0 and p[1][0] == '.':
+ # handle relative filenames
+ p[1] = p[0] + p[1]
+ if p[1].startswith(p[0]):
+ cur, end = p
+ while cur != end:
+ cur = step(cur, end)
+ word, steps = applyRuleList(word, lf(cur))
+ if debug > 2:
+ db += steps
+ if debug > 1:
+ db += cur + ': ' + word + '\n'
+ else:
+ # if the members of a pair aren't directly related, it can't do
+ # anything
+ raise Exception(p[1] + ' does not start with ' + p[0])
+ if debug == 1:
+ db += p[1] + ': ' + word + '\n'
+ return word, db
diff --git a/conlang/soundChanger.py b/conlang/soundChanger.py
index 8ccee94..72c282d 100644
--- a/conlang/soundChanger.py
+++ b/conlang/soundChanger.py
@@ -1,175 +1,97 @@
-#! /usr/bin/env python3.4 # lint:ok
-
import regex
from . import workers
-# rules are stored as dicts:
-# {'from': '', 'to': '', 'before': '', 'after': '',
-# 'unbefore': '', 'unafter': ''}
+catMatcher = regex.compile(r'\{(\d*):?(\w*)\}')
-# all cats would be included in a dict with keys being the cat name,
-# and the values being arrays of the members of that cat
-# e.g. {'nasal':['m', 'n', 'ŋ'], 'vlplos':['p', 't', 'k'], ...}
+def catReplace(m, cats):
+ """Replaces categories and numbered categories from cats with regex strings
+ that will match them appropriately"""
+ n, c = m.groups()
+ if c in cats:
+ if n == '':
+ return '(' + '|'.join(cats[c]) + ')'
+ return '(?P' + '|'.join(cats[c]) + ')'
+ return m.group(0)
-def parseSoundChange(l, cats):
- out = {}
- if len(l.split(' = ')) == 2:
- out['catName'] = l.split(' = ')[0].strip()
- category = l.split(' = ')[1]
- for c in cats:
- category = category.replace('{' + c + '}',
- ' '.join(x if x else '0' for x in cats[c]))
- out['category'] = category.split()
- else:
- try:
- out['from'] = l.split(' > ')[0].split(' / ')[0].split(' ! ')[0]
- if out['from'] == '0':
- out['from'] = ''
- out['from'] = out['from'].replace('#', r'((?<=^|\s)|(?=$|\s))')
- out['to'] = l.split(' > ')[1].split(' / ')[0].split(' ! ')[0]
- out['to'].replace('#', r'((?<=^|\s)|(?=$|\s))')
- if out['to'] == '0':
- out['to'] = ''
- except IndexError:
- out['to'] = ''
- try:
- out['before'] = l.split(' / ')[1].split('_')[0]
- out['before'] = out['before'].replace('#', r'((?<=^|\s)|(?=$|\s))')
- out['after'] = l.split(' / ')[1].split('_')[1].split(' ! ')[0]
- out['after'] = out['after'].replace('#', r'((?<=^|\s)|(?=$|\s))')
- except IndexError:
- pass
- try:
- out['unbefore'] = l.split(' ! ')[1].split('_')[0]
- out['unbefore'] = out['unbefore'].replace('#', r'((?<=^|\s)|(?=$|\s))')
- out['unafter'] = l.split(' ! ')[1].split('_')[1]
- out['unafter'] = out['unafter'].replace('#', r'((?<=^|\s)|(?=$|\s))')
- except IndexError:
- pass
- return out
-
-def checkNumCats(m, ruleStr, cats):
- # now u test the match to see if it matches with numbered categories
- # matched = m.group(1)
- catIndex = []
- iNc = 0
- for c in regex.finditer(r'\{(\d+):([^}]*)}', ruleStr):
- # now what? maybe i should have it search the matched
- # string for this category, find which it is, and determine
- # which index for which numbered category
- cNum = int(c.group(1))
- cCat = c.group(2)
- cNc = 'nc' + str(iNc)
- cMatch = m.group(cNc)
- if cCat in cats:
- if len(catIndex) > cNum and catIndex[cNum] is not None:
- if cats[cCat][catIndex[cNum]] != cMatch:
- return None
- # if anything has the wrong number,
- # quit out of this match
- else:
- workers.addPad(catIndex, cNum,
- cats[cCat].index(cMatch))
- iNc += 1
- return catIndex
+def toCatReplace(m, cats, indices):
+ """Replaces numbered categories in a match with the appropriate element of
+ a category"""
+ n, c = m.groups()
+ if c in cats and n != '':
+ # treat '0' as '', to allow for categories with gaps
+ if cats[c][indices['nc' + n]] == '0':
+ return ''
+ return cats[c][indices['nc' + n]]
+ return m.group(0)
-def findMatch(word, rule, cats):
- """Applies a specified rule to the given word,\
- making category replacements if necessary"""
- # create match, ignoring numbered categories
- matchStr = '('
+def findMatches(word, rule, cats):
+ """Finds all matches of rule in word"""
+ # First, generate a regex pattern to search for:
+ pattern = ''
if 'before' in rule and rule['before'] != '':
- matchStr += '(?<=(?P' + rule['before'] + '))'
+ pattern += '(?<=(?P' + rule['before'] + '))'
if 'unbefore' in rule and rule['unbefore'] != '':
- matchStr += '(?' + rule['from'] + ')'
+ pattern += '(?' + rule['from'] + ')'
if 'after' in rule and rule['after'] != '':
- matchStr += '(?=(?P' + rule['after'] + '))'
+ pattern += '(?=(?P' + rule['after'] + '))'
if 'unafter' in rule and rule['unafter'] != '':
- matchStr += '(?!' + rule['unafter'] + ')'
- matchStr += ')'
- ruleStr = matchStr
- # replace categories
- nc = [0]
-
- def catReplace(m):
- if m.group(2) in cats:
- out = '('
- if m.group(1) == '':
- out += '|'.join(cats[m.group(2)])
- else:
- out += '?P' + '|'.join(cats[m.group(2)])
- nc[0] += 1
- return out + ')'
- else:
- return m.group(0)
-
- matchStr = regex.sub(r'\{(\d*):?([^}]*)}', catReplace, matchStr)
- try:
- match = list(regex.finditer(matchStr, word))
- except regex._regex_core.error as e:
- raise type(e)(e.args + (matchStr, word))
+ pattern += '(?!' + rule['unafter'] + ')'
+ # Next, replace all the categories in the pattern with real regex
+ pattern = catMatcher.sub(lambda m: catReplace(m, cats), pattern)
+ # Now pattern is a valid regex
+ matches = []
catIndex = []
- if nc[0] and match != []:
- for m in range(len(match)):
- catIndex.append(checkNumCats(match[m], ruleStr, cats))
- return (match, matchStr, ruleStr, catIndex)
+ for m in regex.finditer(pattern, word):
+ # For each match, check that the numbered categories match, and
+ # populate catIndex with the indices associate with each one
+ try:
+ catIndex.append(numberedCategories(m, cats))
+ matches.append(m)
+ except ValueError:
+ # The numbered categories didn't match
+ pass
+ # Each match in matches is a valid match, since those that had mismatching
+ # numbered categories were never added to it.
+ return matches, catIndex
+
+
+def numberedCategories(m, cats):
+ """Returns a dict of the indices associated with each numbered category, of
+ the form {'nc0': 1, 'nc1': 2, ...}"""
+ captures = m.capturesdict()
+ indices = {}
+ for nc in captures:
+ if nc.startswith('nc'):
+ n, c = nc.split('_', 1)
+ if len(set(captures[nc])) > 1:
+ # the same number/category pair matched multiple strings
+ raise ValueError()
+ if n in indices:
+ if cats[c][indices[n]] != captures[nc][0]:
+ # a previous number/category pair had a different index
+ raise ValueError()
+ else:
+ indices[n] = cats[c].index(captures[nc][0])
+ return indices
def applyRule(word, rule, cats):
- match, matchStr, ruleStr, catIndex = findMatch(word, rule, cats)
- if match == []:
+ """Applies the sound change specified by rule to word"""
+ matches, catIndex = findMatches(word, rule, cats)
+ if len(matches) == 0:
+ # there were no matches, so no changes need to be applied
return word
- i = [-1]
-
- def fromToTo(m):
- # now, replace from (m.group('from')) with to
- toStr = rule['to']
-
- i[0] += 1
-
- if checkNumCats(m, ruleStr, cats) is None:
- return m.group()
-
- def toCatReplace(c):
- cNum = int(c.group(1))
- cCat = c.group(2)
- if cCat in cats:
- if len(catIndex[i[0]]) > cNum and catIndex[i[0]][cNum] is not None:
- if cats[cCat][catIndex[i[0]][cNum]] == '0':
- return ''
- return cats[cCat][catIndex[i[0]][cNum]]
-
- return regex.sub(r'\{(\d+):([^}]*)}', toCatReplace, toStr)
-
- word = regex.sub(matchStr, fromToTo, word)
+ # starting from the end of the string, replace each match with the output
+ # of the rule
+ for match, indices in zip(reversed(matches), reversed(catIndex)):
+ # produce the appropriate replacement, given numbered categories, and
+ # insert it into the word
+ word = workers.sliceReplace(word, match.span(),
+ catMatcher.sub(lambda m: toCatReplace(m, cats, indices),
+ rule['to']))
return word
-
-def applyRules(word, soundChanges):
- cats = {}
- debug = ''
- try:
- for l in soundChanges:
- rc = parseSoundChange(l, cats)
- if 'catName' in rc:
- cats[rc['catName']] = rc['category']
- else:
- word = applyRule(word, rc, cats)
- debug += l + ' ' + word + '\n'
- except TypeError:
- for w in range(len(word)):
- word[w] = applyRules(word[w], soundChanges)
- return word, debug
-
-
-def getCats(soundChanges):
- cats = {}
- for l in soundChanges:
- rc = parseSoundChange(l, cats)
- if 'catName' in rc:
- cats[rc['catName']] = rc['category']
- return cats
\ No newline at end of file
diff --git a/conlang/test.py b/conlang/test.py
deleted file mode 100644
index 074f135..0000000
--- a/conlang/test.py
+++ /dev/null
@@ -1,14 +0,0 @@
-from soundChanger import applyRule
-cats = {'V': ['u', 'z', 'i', 'ɯ', 'a'],
- 'F': ['f', 's', 'ç', 'x', 'ħ']}
-rules = [{'from': '{0:F}', 'to': '{0:V}', 'before': '{F}', 'after': '{F}'},
- {'from': '{0:V}', 'to': '{0:F}', 'before': '{V}', 'after': '{V}'}]
-word = input()
-#word = 'ħsfuza'
-
-print(word)
-
-for rule in rules:
- print(rule)
- word = applyRule(word, rule, cats)
- print(word)
diff --git a/conlang/test.txt b/conlang/test.txt
deleted file mode 100644
index 7cca941..0000000
--- a/conlang/test.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-from soundChanger import applyRule
-cats = {'{V}': ['u', 'z', 'i', 'ɯ', 'a'],
- '{F}': ['f', 's', 'ç', 'x', 'ħ']}
-rules = [{'from':
-cats = {'{V}': ['a', 'e', 'i', 'o', 'u']}
-applyRule('aciece', rule, cats)
diff --git a/conlang/workers.py b/conlang/workers.py
index bf2ebb8..fedc04e 100644
--- a/conlang/workers.py
+++ b/conlang/workers.py
@@ -1,9 +1,12 @@
-#! /usr/bin/env python3.3 # lint:ok
+import cgitb
+import sys
+import os
+FILE_PATH = '/mit/sashacf/web_scripts/soundchanger'
def addPad(l, n, item):
- '''adds item to list l at position n, padding l with the value None, if
- necessary'''
+ """Adds item to list l at position n, padding l with the value None, if
+ necessary"""
if n >= len(l):
l += [None] * (n + 1 - len(l))
if l[n] is None:
@@ -11,14 +14,27 @@ def addPad(l, n, item):
def flipDict(d):
- '''returns a dict with values and keys reversed'''
+ """Returns a dict with values and keys reversed"""
return {v: k for k, v in d.items()}
-class LoopBreak(Exception):
- pass
+class Reencoder():
+ """A stream that uses 'xmlcharrefreplace' to reencode it's output."""
+
+ def __init__(self, stream=sys.__stdout__):
+ self.stream = stream
+
+ def write(self, *a):
+ return self.stream.write(*(reencode(s) for s in a))
+
+ def flush(self):
+ return self.stream.flush()
+
+
+reencode = lambda s: s.encode('ascii', 'xmlcharrefreplace').decode()
def sliceReplace(word, sl, repl):
- '''returns word with the slice indicated by sl replaced with repl'''
+ """Returns word with the slice indicated by sl replaced with repl"""
return word[:sl[0]] + repl + word[sl[1]:]
+
diff --git a/core.py b/core.py
deleted file mode 100755
index f8ec57a..0000000
--- a/core.py
+++ /dev/null
@@ -1,27 +0,0 @@
-#!./.interpreter.sh
-
-import cgitb
-import sys
-cgitb.enable()
-
-import asc
-
-FILE_PATH = '/mit/sashacf/web_scripts/soundchanger'
-
-class Reencoder():
-
- def __init__(self, stream=sys.__stdout__):
- self.stream = stream
-
- def write(self, *a):
- return self.stream.write(*(reencode(s) for s in a))
-
- def flush(self):
- return self.stream.flush()
-
-reencode = lambda s: s.encode('ascii', 'xmlcharrefreplace').decode()
-
-if __name__ == '__main__':
- print('Content-Type: text/html')
- print('')
- print(reencode(asc.asc('sˈezuza sˈewa', [('prt.west.sajura', 'prt.west.sajura.purrub.middle.em.modern.orth')], 0, '/mit/sashacf/web_scripts/soundchanger')[0]))
\ No newline at end of file
diff --git a/soundchanger.nja b/soundchanger.nja
deleted file mode 100644
index 13b782a..0000000
--- a/soundchanger.nja
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "venv": "",
- "project-type": "Import from sources",
- "name": "soundchanger",
- "license": "GNU General Public License v3",
- "description": ""
-}
\ No newline at end of file