Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quantum-kittens
GitHub Repository: quantum-kittens/platypus
Path: blob/main/converter/postbuild.ts
3338 views
1
import * as fs from 'fs'
2
import * as path from 'path'
3
4
import { JSDOM as JSDom } from 'jsdom'
5
import * as yaml from 'js-yaml'
6
7
import { Course } from '@mathigon/studio/server/interfaces'
8
import { CONTENT, OUTPUT, loadYAML, writeFile } from '@mathigon/studio/build/utilities'
9
import { parseYAML } from '@mathigon/studio/build/markdown'
10
import { decode } from 'html-entities'
11
12
import {
13
translationsLanguages,
14
workingContentPath,
15
workingTranslationsPath
16
} from './common'
17
import generateJsonSitemap from './sitemap-to-json'
18
19
const COURSES = fs.readdirSync(CONTENT)
20
.filter(id => id !== 'shared' && !id.includes('.') && !id.startsWith('_'))
21
22
const loadJSON = function (file: string) {
23
if (!fs.existsSync(file)) return undefined
24
return JSON.parse(fs.readFileSync(file, 'utf8')) as unknown
25
}
26
27
const getIndexPath = function(courseId) {
28
return `${workingContentPath}/${courseId}/index.yaml`
29
}
30
31
const getSharedPath = function (language: string = 'en') {
32
return language == 'en'
33
? path.join(workingContentPath, 'shared')
34
: path.join(workingTranslationsPath, language, 'shared')
35
}
36
37
const findCourse = function (courseId: string, locale: string = 'en'): Course {
38
const course = loadJSON(OUTPUT + `/content/${courseId}/data_${locale}.json`) as Course
39
if (!course) return undefined;
40
return course
41
}
42
43
const findEquationFromTitle = function(title) {
44
return title.match(/\$(.*?)\$/g)
45
}
46
47
const replaceEquationByMathjax = function(title, mathjaxEquation) {
48
return title.replace(/\$(.*?)\$/g, mathjaxEquation)
49
}
50
51
const findIndexFromCourse = function(path) {
52
const indexCourse = loadYAML(path)
53
if (Object.entries(indexCourse).length === 0) {
54
return undefined
55
}
56
return indexCourse
57
}
58
59
const insertSections = (content: object, document: HTMLDocument, includeHtml: boolean): object => {
60
Object.keys(content).forEach((key) => {
61
const valueObj = content[key]
62
let sectionIds = new Set()
63
let node: any = document.getElementById(key)
64
65
if (node) {
66
// search by id attribute
67
sectionIds.add(node.closest('q-section').getAttribute('data-id'))
68
node.removeAttribute('id')
69
} else {
70
// search by (x-gloss) xid attribute
71
let nodes = document.querySelectorAll(`x-gloss[xid="${key}"]`)
72
if (nodes.length) {
73
node = nodes[0]
74
nodes.forEach(n => {
75
sectionIds.add(n.closest('q-section').getAttribute('data-id'))
76
})
77
} else {
78
// search by class attribute
79
nodes = document.querySelectorAll(`mjx-container .${key}`)
80
node = nodes.length ? nodes[0] : null
81
nodes.forEach(n => {
82
sectionIds.add(n.closest('q-section').getAttribute('data-id'))
83
});
84
}
85
}
86
87
if (includeHtml) {
88
valueObj.html = node ? node.outerHTML : ''
89
}
90
valueObj.sections = []
91
92
if (sectionIds.size) {
93
try {
94
sectionIds.forEach(sectionId => {
95
if (sectionId) {
96
valueObj.sections.push(sectionId)
97
}
98
})
99
} catch (e) {
100
console.warn(e)
101
}
102
}
103
})
104
105
return content
106
}
107
108
const parseSection = function(section, store) {
109
if(section.subsections?.length) {
110
section.subsections = section.subsections.map(subsection => parseSection(subsection, store))
111
}
112
113
const code = findEquationFromTitle(section.title)
114
if(!code) {
115
return section
116
}
117
118
const codeCleaned = parseParagraph(code[0])
119
const codeId = getId(codeCleaned)
120
if (store[codeId]) {
121
section.title = replaceEquationByMathjax(section.title, store[codeId])
122
}
123
return section
124
}
125
126
const updateIndexYaml = async function() {
127
// Get Mathjax cache from Mathigon build
128
const cacheFile = path.join(process.env.HOME, '/.mathjax-cache')
129
if(!fs.existsSync(cacheFile)) {
130
return undefined
131
}
132
const mathJaxStore = JSON.parse(fs.readFileSync(cacheFile, 'utf8'))
133
134
const indexCoursePaths = COURSES.map(courseId => getIndexPath(courseId))
135
const indexCourses = indexCoursePaths.map(indexCoursePath => findIndexFromCourse(indexCoursePath))
136
const indexCoursesParsed = indexCourses.map(index => {
137
if(!index) {
138
return undefined
139
}
140
141
const newIndex = {}
142
const moduleIds = Object.keys(index)
143
const modules = moduleIds.map(moduleId => index[moduleId])
144
const modulesParsed = modules.map(module =>
145
module.map(section => parseSection(section, mathJaxStore)))
146
moduleIds.map((moduleId, index) => newIndex[moduleId] = modulesParsed[index])
147
return newIndex
148
})
149
150
for(let indexCoursePath of indexCoursePaths) {
151
const indexCourse = indexCoursesParsed[indexCoursePaths.indexOf(indexCoursePath)]
152
if(indexCourse) {
153
await writeFile(indexCoursePath, yaml.dump(indexCourse, {sortKeys: true}))
154
}
155
}
156
}
157
158
const updateSharedYaml = async function(language: string = 'en') {
159
let courseHtml = ''
160
COURSES.forEach(courseId => {
161
const course = findCourse(courseId, language)
162
if (course) {
163
Object.keys(course.steps).forEach(stepId => {
164
const section = course?.sections.find(s => s.steps.indexOf(stepId) > -1)
165
// insert a section id for easier retrieval later
166
courseHtml += `<q-section data-id="${section.id}">${course.steps[stepId].html}</q-section>`
167
})
168
}
169
})
170
171
const document = new JSDom(`<!DOCTYPE html><body>${courseHtml}</body>`).window.document
172
const sharedPath = getSharedPath(language)
173
174
// update notations.yaml with section IDs and html fragment
175
console.log(`updating notations yaml [${language}]`)
176
const notationsYaml = path.join(sharedPath, 'notations.yaml')
177
const notations = insertSections((loadYAML(notationsYaml) || {}) as object, document, true)
178
await writeFile(notationsYaml, yaml.dump(notations, {sortKeys: true}))
179
180
// update glossary.yaml with section IDs
181
console.log(`updating glossary yaml [${language}]`)
182
const glossaryYaml = path.join(sharedPath, 'glossary.yaml')
183
const glossary = insertSections((loadYAML(glossaryYaml) || {}) as object, document, false)
184
await writeFile(glossaryYaml, yaml.dump(glossary, {sortKeys: true}))
185
186
// update universal notations
187
console.log(`updating universal-notations yaml [${language}]`)
188
const universal = await parseYAML(sharedPath, 'universal-notations.yaml', language, 'notation')
189
const startIndex = '<p>'.length
190
const endIndex = '</p>'.length
191
192
for (let notes in universal) {
193
let n = universal[notes].notation
194
if (n.startsWith('<p>')) {
195
n = n.substring(startIndex)
196
}
197
if (n.endsWith('</p>')) {
198
n = n.substring(0, n.length - endIndex)
199
}
200
universal[notes].notation = n
201
}
202
203
const universalYaml = path.join(sharedPath, 'universal.yaml')
204
await writeFile(universalYaml, yaml.dump(universal, {sortKeys: true}))
205
}
206
207
208
/** Mathigon methods from mathjax.js */
209
210
/** This method differs from the original one in two things:
211
* 1.- to simplify the logic the key always finishes with truehtml
212
* 2.- there is a replace that removes \; by ;
213
*/
214
const getId = function(code) {
215
return `${decode(code)}truehtml`.replace(/\\;/g, ';')
216
}
217
218
/** Mathigon methods from renderer.js */
219
220
/** This method differs from the original to avoid the mathigon widgets and GitHub Emoji replaces */
221
const parseParagraph = function(text) {
222
text = inlineEquations(text);
223
224
// Replace non-breaking space and escaped $s.
225
return text.replace(/\\ /g, '&nbsp;').replace(/\\\$/g, '$');
226
}
227
228
/** Render inline LaTeX equations using $x^2$. */
229
function inlineEquations(text) {
230
// We want to match $a$ strings, except
231
// * the closing $ is immediately followed by a word character (e.g. currencies)
232
// * the opening $ is prefixed with a \ (for custom override)
233
// * they start with ${} (for variables)
234
return text.replace(/(^|[^\\])\$([^{][^$]*?)\$($|[^\w])/g, (_, prefix, body, suffix) => {
235
return prefix + decode(body) + suffix;
236
});
237
}
238
239
translationsLanguages.forEach(async(language) => {
240
updateSharedYaml(language)
241
})
242
243
updateIndexYaml()
244
245
generateJsonSitemap(
246
path.join(__dirname, '../public/sitemap.xml'),
247
path.join(__dirname, '../public/sitemap.json')
248
)
249
250