Skip to content

Commit c02fe42

Browse files
committed
Start adding tooling to work with Markdown instead of Asciidoc
1 parent 19ae586 commit c02fe42

File tree

6 files changed

+425
-0
lines changed

6 files changed

+425
-0
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
"dependencies": {
1212
"acorn": "^5.1.0",
1313
"codemirror": "^5.25.0",
14+
"hjson": "^3.1.0",
1415
"jszip": "^2.5.0",
16+
"markdown-it": "^8.4.0",
1517
"uglify-js": "^2.0.0"
1618
},
1719
"devDependencies": {

src/chapter.html

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<!doctype html>
2+
<head>
3+
<meta charset="utf-8">
4+
<meta name="viewport" content="width=device-width, initial-scale=1">
5+
<title><<t $in.title>> :: Eloquent JavaScript</title>
6+
<link rel=stylesheet href="js/node_modules/codemirror/lib/codemirror.css">
7+
<script src="js/acorn_codemirror.js"></script>
8+
<link rel=stylesheet href="css/ejs.css">
9+
<script src="js/sandbox.js"></script>
10+
<script src="js/ejs.js"></script>
11+
<<if $in.chap_num || $in.load_files>>
12+
<script>
13+
<<if $in.chap_num>>var chapNum = <<t chap_num>>;<</if>>
14+
<<if $in.load_files>>var sandboxLoadFiles = <<t JSON.stringify($in.load_files)>>;<</if>>
15+
</script>
16+
<</if>>
17+
</head>
18+
19+
<article>
20+
<nav>
21+
<<if $in.prev_link>><a href="<<t $in.prev_link>>.html" title="previous chapter"></a><</if>>
22+
<a href="index.html" title="cover"></a>
23+
<<if $in.next_link>><a href="<<t $in.next_link>>.html" title="next chapter"></a><</if>>
24+
</nav>
25+
26+
<h1<<if $in.id>> id="<<t $in.id>>"<</if>>><<if $in.chap_num>><div class=chap_num>Chapter <<t $in.chap_num>></div><</if>><<t $in.title>></h1>
27+
28+
<<h $in.content>>
29+
30+
<nav>
31+
<<if $in.prev_link>><a href="<<t $in.prev_link>>.html" title="previous chapter"></a><</if>>
32+
<a href="index.html" title="cover"></a>
33+
<<if $in.next_link>><a href="<<t $in.next_link>>.html" title="next chapter"></a><</if>>
34+
</nav>
35+
</article>

src/convert.js

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
let fs = require("fs")
2+
3+
let text = fs.readFileSync(process.argv[2], "utf8")
4+
5+
function processIndexTerm(term) {
6+
term = term.replace(/\+\+\+,\+\+\+/g, "×")
7+
let terms = term.split(",").map(t => /\W/.test(t) ? JSON.stringify(t.replace(/×/g, ",").replace(/\s/g, " ")) : t)
8+
return terms.length == 1 ? terms[0] : "[" + terms.join(", ") + "]"
9+
}
10+
11+
text = text
12+
.replace(/^(:\w+:\s*.+\n)+/, function(meta) {
13+
let re = /(?:^|\n):(\w+):\s*(.+)/g, m
14+
let tag = "{{meta"
15+
while (m = re.exec(meta))
16+
tag += ", " + m[1] + ": " + m[2]
17+
return tag + "}}\n"
18+
})
19+
.replace(/\n(=+) (.*?) =+\n/g, function(_, depth, title) {
20+
return "\n" + "#".repeat(depth.length) + " " + title + "\n"
21+
})
22+
.replace(/\nimage::([^\]]+)\[(.*?)\]/g, function(_, url, meta) {
23+
return "\n{{figure, url: " + JSON.stringify(url) + ", " + meta.replace(/="/g, ": \"") + "}}"
24+
})
25+
.replace(/\n(\[chapterquote=.*?\]\n)?\[quote, ([^\]]+)\]\n____\n([^]*?)____\n/g, function(_, chapter, author, content) {
26+
let match = /([^,]+), (.+)/.exec(author), source = null
27+
if (match) { source = match[2]; author = match[1] }
28+
return "\n{{quote" + (chapter ? ", chapter: true" : "") + ", author: " + JSON.stringify(author) +
29+
(source ? ", source: " + JSON.stringify(source) : "") + "\n\n" + content + "\n}}\n"
30+
})
31+
.replace(/\n\n+((?:(?!\n\n)[^])*?\(\(\((?:(?!\n\n)[^])*)/g, function(_, para) {
32+
let terms = []
33+
para = para.replace(/\(\(\(((?:\([^\)]*\)|[^])*?)\)\)\)/g, function(_, content) {
34+
terms.push(content)
35+
return ""
36+
}).replace(/^\s*/, "")
37+
return "\n\n{{index " + terms.map(processIndexTerm).join(", ") + "}}\n\n" + para
38+
})
39+
.replace(/\bindexsee:\[(.*?),\s*(.*?)\]\s*/g, function(_, term, ref) {
40+
return "{{indexsee " + processIndexTerm(term) + ", " + processIndexTerm(ref) + "}}\n\n"
41+
})
42+
.replace(/\n(?:\[sandbox="(.*?)"\]\n)?(?:\[source,(.*?)\]\n)?(\[focus=.*?\]\n)?(?:---+|\+\+\++)\n([^]*?)\n(?:---+|\+\+\++)\n/g, function(_, sandbox, type, focus, content) {
43+
let params = []
44+
if (type != "javascript") params.push(type || "null")
45+
if (focus) params.push("focus")
46+
if (sandbox) params.push("sandbox-" + sandbox)
47+
return "\n```" + params.join(" ") + "\n" + content + "\n```\n"
48+
})
49+
.replace(/\n\/\/ (?:(start_code)|test: (.*)|include_code (.*))/g, function(_, startCode, test, includeCode) {
50+
if (startCode) return "\n{{startCode}}"
51+
if (test) return "\n{{test " + test + "}}"
52+
return "\n{{includeCode " + JSON.stringify(includeCode) + "}}"
53+
})
54+
.replace(/\blink:([^\[]+)\[(.*?)\]/g, function(_, url, content) {
55+
return "[" + content + "](" + url + ")"
56+
})
57+
.replace(/\nifdef::(\w+?)_target\[\]\n([^]*?)\nendif::.*/g, function(_, type, content) {
58+
return "\n{{if " + type + "\n" + content + "\n}}"
59+
})
60+
.replace(/\+\+(?! |\))((?:(?!\n\n)[^])+)\+\+/g, function(_, text) {
61+
return "_" + text + "_"
62+
})
63+
.replace(/__((?:(?!\n\n)[^])+)__/g, function(_, text) {
64+
return "_" + text + "_"
65+
})
66+
67+
console.log(text)

src/markdown.js

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
const PJSON = require("./pseudo_json")
2+
const markdownIt = require("markdown-it")
3+
4+
function parseData(str) {
5+
let tag = /^\s*(\w+)\s*(,\s*)?/.exec(str), obj
6+
if (!tag) return null
7+
if (tag[0].length == str.length) {
8+
obj = {}
9+
} else if (tag[2]) {
10+
try { obj = PJSON.parse("{" + str.slice(tag[0].length) + "}") }
11+
catch(_) { return null }
12+
} else {
13+
obj = {}
14+
try { obj.args = PJSON.parse("[" + str.slice(tag[0].length) + "]") }
15+
catch(_) { return null }
16+
}
17+
obj._ = tag[1]
18+
return obj
19+
}
20+
21+
function parseBlockMeta(state, startLine, endLine) {
22+
let pos = state.bMarks[startLine] + state.tShift[startLine]
23+
let max = state.eMarks[startLine]
24+
// Check for code block indentation or end of input
25+
if (state.sCount[startLine] - state.blkIndent >= 4 || pos + 4 > max) return false
26+
27+
// Test for `{{` opening marker
28+
if (state.src.charCodeAt(pos) != 123 || state.src.charCodeAt(pos + 1) != 123) return false
29+
30+
let content = state.src.slice(pos + 2, max), single
31+
32+
if (single = /\}\}\s*/.exec(content)) {
33+
let data = parseData(content.slice(0, single.index))
34+
if (!data) return false
35+
let token = state.push("meta", null, 0)
36+
token.map = [startLine, startLine + 1]
37+
token.attrs = [["data", data]]
38+
state.line++
39+
return true
40+
}
41+
42+
let data = parseData(content)
43+
if (!data) return false
44+
45+
let line = startLine + 1, depth = 0
46+
for (; line < endLine; line++) {
47+
if (line == endLine) throw new SyntaxError("Unterminated meta block")
48+
let start = state.bMarks[line] + state.tShift[line]
49+
let after = state.src.slice(start, start + 2)
50+
if (after == "{{" && !/\}\}\s*$/.test(state.src.slice(start, state.eMarks[line]))) depth++
51+
else if (after == "}}") {
52+
if (depth) depth--
53+
else break
54+
}
55+
}
56+
57+
let token = state.push("meta_open", null, 1)
58+
token.map = [startLine, line + 1]
59+
token.attrs = [["data", data]]
60+
state.md.block.tokenize(state, startLine + 1, line)
61+
state.push("meta_close", null, -1)
62+
state.line = line + 1
63+
64+
return true
65+
}
66+
67+
function parseInlineMeta(state) {
68+
if (state.src.charCodeAt(state.pos) != 91) return false // '['
69+
70+
let max = state.posMax
71+
let end = state.md.helpers.parseLinkLabel(state, state.pos, false)
72+
if (end < 0) return false
73+
74+
let pos = end + 1
75+
if (pos >= max || state.src.charCodeAt(pos) != 123) return false // '{'
76+
77+
let metaEnd = pos + 1, depth = 0
78+
for (;; metaEnd++) {
79+
if (metaEnd == max) return false
80+
let code = state.src.charCodeAt(metaEnd)
81+
if (code == 125) { // '}'
82+
if (depth) depth--
83+
else break
84+
} else if (code == 123) {
85+
depth++
86+
}
87+
}
88+
89+
let data = parseData(state.src.slice(pos + 1, metaEnd))
90+
if (!data) return false
91+
92+
state.pos++
93+
state.posMax = end
94+
state.push("meta_open", null, 1).attrs = [["data", data]]
95+
state.md.inline.tokenize(state)
96+
state.push("meta_close", null, -1)
97+
state.pos = metaEnd + 1
98+
state.posMax = max
99+
100+
return true
101+
}
102+
103+
function parseIndexTerm(state) {
104+
let max = state.posMax
105+
// Check for opening '(('
106+
if (state.pos >= max + 4 || state.src.charCodeAt(state.pos) != 40 || state.src.charCodeAt(state.pos + 1) != 40) return false
107+
108+
let start = state.pos + 2, end = start
109+
for (;; end++) {
110+
if (end >= max - 1) return false
111+
if (state.src.charCodeAt(end) == 41 && state.src.charCodeAt(end + 1)) break
112+
}
113+
114+
let term = state.src.slice(start, end)
115+
116+
state.push("meta", null, 0).attrs = [["data", {_: "index", args: [term]}]]
117+
state.pending += term
118+
state.pos = end + 2
119+
return true
120+
}
121+
122+
let TERMINATOR_RE = /[\n!#$%&*+\-:<=>@[\\\]^_`{}~]|\(\(/
123+
124+
function newText(state) {
125+
let len = state.src.slice(state.pos).search(TERMINATOR_RE)
126+
if (len == 0) return false
127+
if (len == -1) len = state.src.length - state.pos
128+
state.pending += state.src.slice(state.pos, state.pos + len)
129+
state.pos += len
130+
return true
131+
}
132+
133+
function plugin(md) {
134+
md.block.ruler.before("code", "meta", parseBlockMeta)
135+
md.inline.ruler.before("link", "meta", parseInlineMeta)
136+
md.inline.ruler.at("text", newText)
137+
md.inline.ruler.before("strikethrough", "index_term", parseIndexTerm)
138+
}
139+
140+
module.exports = markdownIt().use(plugin)

src/pseudo_json.js

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
class Stream {
2+
constructor(str) {
3+
this.str = str
4+
this.pos = 0
5+
}
6+
7+
err(msg) {
8+
throw new SyntaxError(msg + " at " + this.pos + " in " + JSON.stringify(this.str))
9+
}
10+
11+
space() {
12+
for (;;) {
13+
let next = this.next
14+
if (next == 32 || next == 9 || next == 10 || next == 13) this.pos++
15+
else break
16+
}
17+
}
18+
19+
get next() {
20+
return this.str.charCodeAt(this.pos)
21+
}
22+
23+
ahead(n) {
24+
this.pos += n
25+
this.space()
26+
}
27+
}
28+
29+
exports.parse = function(str) {
30+
let stream = new Stream(str)
31+
stream.space()
32+
let value = parseValue(stream)
33+
if (stream.pos != stream.str.length) stream.err("Extra characters at end of input")
34+
return value
35+
}
36+
37+
function parseValue(stream) {
38+
let next = stream.next
39+
if (next == 123) return parseObj(stream)
40+
if (next == 91) return parseArr(stream)
41+
if (next == 34) return parseStr(stream)
42+
return parseWord(stream)
43+
}
44+
45+
function parseObj(stream) {
46+
stream.ahead(1)
47+
let obj = {}
48+
for (;;) {
49+
if (stream.next == 125) break
50+
let prop = parseWord(stream, true)
51+
if (stream.next != 58) stream.err("Expected ':'")
52+
stream.ahead(1)
53+
obj[prop] = parseValue(stream)
54+
if (stream.next == 44) stream.ahead(1)
55+
}
56+
stream.ahead(1)
57+
return obj
58+
}
59+
60+
function parseArr(stream) {
61+
stream.ahead(1)
62+
let arr = []
63+
for (;;) {
64+
if (stream.next == 93) break
65+
arr.push(parseValue(stream))
66+
if (stream.next == 44) stream.ahead(1)
67+
}
68+
stream.ahead(1)
69+
return arr
70+
}
71+
72+
function parseStr(stream) {
73+
let start = stream.pos
74+
stream.pos++
75+
for (let escaped = false;;) {
76+
let next = stream.next
77+
stream.pos++
78+
if (next == 34 && !escaped) break
79+
else if (isNaN(next)) stream.error("Unterminated string")
80+
escaped = next == 92
81+
}
82+
stream.space()
83+
return JSON.parse(stream.str.slice(start, stream.pos))
84+
}
85+
86+
function parseWord(stream, prop) {
87+
let start = stream.pos
88+
for (;;) {
89+
let next = stream.next
90+
if ((next >= 97 && next <= 122) || (next >= 65 && next <= 90) || next == 95 || (next >= 48 && next <= 57)) stream.pos++
91+
else break
92+
}
93+
let word = stream.str.slice(start, stream.pos)
94+
stream.space()
95+
if (/^(?:0x[\da-f]+|\d*(?:\.\d*)?(?:[eE][+\-]?\d+)?)$/i.test(word)) return JSON.parse(word)
96+
if (!prop) {
97+
if (word == "true") return true
98+
if (word == "false") return false
99+
}
100+
return word
101+
}

0 commit comments

Comments
 (0)