forked from toolkit-for-ynab/toolkit-for-ynab
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathpopulateFeaturesFiles.py
executable file
·247 lines (198 loc) · 10.3 KB
/
populateFeaturesFiles.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
#!/usr/bin/env python
"""Assemble features from separate directories and prepare files for use in the browser."""
import os
import json
import traceback
import re
try:
from urllib.request import pathname2url
except ImportError:
from urllib import pathname2url
allSettings = []
allFeedChangesContent = set()
previousNames = set()
def checkIndividualSetting(setting, dirName, markDown):
"""Validate feature settings in a given dirName."""
if 'name' not in setting:
raise ValueError('Every setting must have a name property.')
if setting['name'] in previousNames:
raise ValueError('Duplicate setting name: ' + setting['name'])
previousNames.add(setting['name'])
if 'type' not in setting:
raise ValueError('Every setting must have a type property.')
if setting['type'] != 'checkbox' and setting['type'] != 'select':
raise ValueError('Only checkbox and select settings are supported at this time. Pull requests are welcome!')
if 'title' not in setting:
raise ValueError('Every setting must have a title.')
if 'actions' not in setting:
raise ValueError('Every setting must declare actions that happen when the setting is activated.')
for action in setting['actions']:
if not isinstance(setting['actions'][action], list):
raise ValueError('Actions must be declared as an array, for example ["injectCSS", "main.css"].')
if len(setting['actions'][action]) % 2 != 0:
raise ValueError('Actions must have an even number of elements, for example ["injectCSS", "main.css"].')
if setting['type'] == 'checkbox':
if 'true' not in setting['actions'] and 'false' not in setting['actions']:
raise ValueError('Checkbox settings must declare an action for "true" or "false" to have any effect.')
elif setting['type'] == 'select':
# if '1' not in setting['actions']:
# raise ValueError('Select settings must declare an action for "1" to have any effect.')
if len(setting['actions']) < 2:
raise ValueError('Select settings must have more than one option.')
# Apply the defaults.
if 'section' not in setting:
setting['section'] = 'general'
if 'default' not in setting:
setting['default'] = False
if markDown != '':
setting['description'] = markDown
if 'description' not in setting:
setting['description'] = ''
# Give a relative path to the files in the actions section that the settings system can understand
# what URL to load when it takes an action
for action in setting['actions']:
i = 0
while i < len(setting['actions'][action]):
currentAction = setting['actions'][action][i]
if currentAction == 'injectCSS' or currentAction == 'injectScript':
fullPath = os.path.join(dirName, setting['actions'][action][i + 1])
# Convert to / if we're on Windows
fullPath = pathname2url(fullPath)
fullPath = fullPath.replace("./source/common/", "")
setting['actions'][action][i + 1] = fullPath
i += 2
def checkSettingStructure(setting, dirName, markDown):
"""Validate the structure of a given setting or group of settings."""
if isinstance(setting, dict):
checkIndividualSetting(setting, dirName, markDown)
elif isinstance(setting, list):
for individualSetting in setting:
checkIndividualSetting(individualSetting, dirName, markDown)
else:
raise ValueError('Settings must be a single JSON object or an array of JSON objects. '
'See the setting file in hide-age-of-money for an example setting.')
print('[ INFO] Building settings and feed changes files...')
for dirName, subdirList, fileList in os.walk('./source/common/res/features/'):
if ('settings.json' in fileList):
with open(os.path.join(dirName, 'settings.json'), 'r') as settingsFile:
settingsContents = json.loads(settingsFile.read())
if ('description.md' in fileList):
with open(os.path.join(dirName, 'description.md'), 'r') as mdFile:
mdContents = mdFile.read().replace('\n','\n') # convert internal new lines to external newlines
else:
mdContents = ''
# Validate first
try:
checkSettingStructure(settingsContents, dirName, mdContents)
except ValueError:
formatted_lines = traceback.format_exc().splitlines()
print("[ ERROR] Settings error: {0}".format(formatted_lines[-1]))
print("[ ERROR] While processing file: {0}".format(settingsFile.name))
print("--------------------------------------------------------------------------------")
print("[ ERROR] EXTENSION WAS NOT BUILT. Please fix the settings errors and try again.")
print("--------------------------------------------------------------------------------")
print("")
exit(1)
# Ok, we're happy, add it to the output.
if isinstance(settingsContents, dict):
if 'hidden' not in settingsContents or not settingsContents['hidden']:
allSettings.append(settingsContents)
else:
for setting in settingsContents:
if 'hidden' not in settingsContents or not settingsContents['hidden']:
allSettings.append(setting)
# Write the settings file with some helper functions
with open('./source/common/res/features/allSettings.js', 'w') as settingsFile:
settingsFile.write('/* eslint-disable no-unused-vars */\n\n')
settingsFile.write(('/*\n'
' ***********************************************************\n'
' * Warning: This is a file generated by the build process. *\n'
' * *\n'
' * Any changes you make manually will be overwritten *\n'
' * the next time you run ./build or build.bat! *\n'
' ***********************************************************\n'
'*/\n\n'))
settingsFile.write('if (typeof window.ynabToolKit === \'undefined\') { window.ynabToolKit = {}; }\n')
settingsFile.write('''
function getKangoSetting(settingName) {
return new Promise(function (resolve) {
kango.invokeAsync('kango.storage.getItem', settingName, function (data) {
resolve(data);
});
});
}
function setKangoSetting(settingName, data) {
return new Promise(function (resolve) {
kango.invokeAsync('kango.storage.setItem', settingName, data, function () {
resolve('success');
});
});
}
function getKangoStorageKeys() {
return new Promise(function (resolve) {
kango.invokeAsync('kango.storage.getKeys', function (keys) {
resolve(keys);
});
});
}
function getKangoExtensionInfo() {
return kango.getExtensionInfo();
}
function ensureDefaultsAreSet() {
return new Promise(function (resolve) {
getKangoStorageKeys().then(function (storedKeys) {
var promises = [];
ynabToolKit.settings.forEach(function (setting) {
if (storedKeys.indexOf(setting.name) < 0) {
promises.push(setKangoSetting(setting.name, setting.default));
}
});
Promise.all(promises).then(function () {
resolve();
});
});
});
}\n\n''')
settingsFile.write('// eslint-disable-next-line quotes, object-curly-spacing, quote-props\n')
settingsFile.write('window.ynabToolKit.settings = ' + json.dumps(allSettings) + ';\n\n')
settingsFile.write('// We don\'t update these from anywhere else, so go ahead and freeze / seal the object so nothing can be injected.\n')
settingsFile.write('Object.freeze(window.ynabToolKit.settings);\n')
settingsFile.write('Object.seal(window.ynabToolKit.settings);\n')
# Write the feedChanges file
pattern = re.compile(r"^[\s]*(ynabToolKit\..+?)[\s]*=[\s]*\([\s]*function[\s]*\([\s]*\)[\s]*\{.*$", re.MULTILINE)
for dirName, subdirList, fileList in os.walk('./source/common/res/features/'):
if dirName.endswith('shared'):
continue
jsFiles = [f for f in fileList if f.endswith('.js')]
for jsFile in jsFiles:
with open(os.path.join(dirName, jsFile), 'r') as content_file:
content = content_file.read()
result = pattern.search(content)
if result:
allFeedChangesContent.add(result.group(1))
with open('./source/common/res/features/act-on-change/feedChanges.js', 'w') as feedChanges:
feedChanges.write(('/*\n'
' **********************************************************\n'
' * Warning: This is a file generated by the build process. *\n'
' * *\n'
' * Any changes you make manually will be overwritten *\n'
' * the next time you run ./build or build.bat! *\n'
' ***********************************************************\n'
' */\n\n'))
feedChanges.write(('(function poll() {\n'
' if (typeof ynabToolKit.shared !== \'undefined\') {\n'
' ynabToolKit.shared.feedChanges = function (changes) {\n'
' // Python script auto builds up this list of features\n'
' // that will use the mutation observer from actOnChange()\n\n'
' // If a feature doesn\'t need to use observe(), we\n'
' // just let it fail silently\n'))
for feedChangesBlock in allFeedChangesContent:
feedChanges.write('\n try {\n')
feedChanges.write(' if (changes.changedNodes) ' + feedChangesBlock + '.observe(changes.changedNodes);\n')
feedChanges.write(' if (changes.routeChanged) ' + feedChangesBlock + '.onRouteChanged(changes.routeChanged);\n')
feedChanges.write(' } catch (err) { /* ignore */ }\n')
feedChanges.write((' };\n'
' } else {\n'
' setTimeout(poll, 100);\n'
' }\n'
'}());\n'))