datalog-theme 0.10.1 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +127 -0
- data/CITATION.cff +2 -2
- data/README.md +4 -2
- data/_data/i18n/en.yml +55 -3
- data/_data/i18n/es.yml +55 -3
- data/_data/i18n/pt.yml +55 -3
- data/_includes/analytics/dashboard.html +24 -9
- data/_includes/components/academic-dashboard.html +168 -133
- data/_includes/components/author-bio.html +7 -5
- data/_includes/components/author-list.html +6 -5
- data/_includes/components/citation-tools.html +2 -1
- data/_includes/components/content-provenance.html +19 -1
- data/_includes/components/correction-report.html +15 -8
- data/_includes/components/hero.html +22 -15
- data/_includes/components/package-index.html +121 -0
- data/_includes/components/package-install.html +27 -50
- data/_includes/components/post-list.html +14 -0
- data/_includes/components/responsive-image.html +15 -21
- data/_includes/footer.html +1 -1
- data/_includes/head.html +70 -11
- data/_includes/layouts/default/article.html +76 -46
- data/_includes/meta/dataset-json.html +171 -0
- data/_includes/meta/math-config.html +15 -1
- data/_includes/meta/package-json.html +69 -0
- data/_includes/meta/person-json.html +36 -13
- data/_includes/meta/publisher.html +15 -0
- data/_includes/meta/schema.html +108 -23
- data/_includes/meta/scholarly.html +10 -5
- data/_includes/post/related-posts.html +17 -43
- data/_includes/scripts.html +2 -2
- data/_includes/search/index-data.json +10 -1
- data/_includes/search/page.html +11 -6
- data/_layouts/archive.html +67 -0
- data/_layouts/dataset.html +2 -1
- data/_layouts/default.html +13 -11
- data/_layouts/docs.html +53 -0
- data/_layouts/home.html +30 -1
- data/_layouts/notebook.html +5 -1
- data/_layouts/package.html +38 -21
- data/_layouts/portfolio.html +1 -0
- data/_layouts/post.html +21 -62
- data/_layouts/project.html +2 -1
- data/_layouts/research.html +14 -2
- data/_plugins/analytics_dashboard.rb +4 -1
- data/_plugins/archive.rb +114 -0
- data/_plugins/authors.rb +50 -1
- data/_plugins/citation_exports.rb +12 -1
- data/_plugins/config_validator.rb +77 -2
- data/_plugins/correction_fallback.rb +110 -0
- data/_plugins/image_optimizer.rb +129 -22
- data/_plugins/math_preprocessor.rb +7 -48
- data/_plugins/notebook_converter.rb +44 -5
- data/_plugins/packages.rb +84 -0
- data/_plugins/page_dates.rb +37 -0
- data/_plugins/references.rb +16 -2
- data/_plugins/related_posts.rb +149 -0
- data/_plugins/search_sections.rb +110 -0
- data/_plugins/site_identity.rb +42 -0
- data/_plugins/social_cards.rb +17 -0
- data/_sass/_academic-dashboard.scss +18 -30
- data/_sass/_base.scss +2 -0
- data/_sass/_components.scss +21 -23
- data/_sass/_docs.scss +143 -0
- data/_sass/_features.scss +6 -0
- data/_sass/_layout.scss +65 -6
- data/_sass/_mathematical.scss +61 -19
- data/_sass/_notebooks.scss +1 -32
- data/_sass/_package-docs.scss +6 -9
- data/_sass/_post-components.scss +23 -23
- data/_sass/_print.scss +1 -1
- data/_sass/_search-page.scss +75 -0
- data/_sass/_search.scss +17 -26
- data/_sass/_syntax-highlighting.scss +23 -1
- data/_sass/_theme.scss +1 -0
- data/_sass/_utilities.scss +87 -40
- data/_sass/_variables.scss +31 -29
- data/assets/css/main.scss +2 -1
- data/assets/js/dist/academic.js +1 -1
- data/assets/js/dist/analytics-dashboard.js +1 -1
- data/assets/js/dist/chunks/chunk-TNVD6UAM.js +1 -0
- data/assets/js/dist/comments.js +1 -1
- data/assets/js/dist/contact.js +1 -1
- data/assets/js/dist/core.js +1 -1
- data/assets/js/dist/corrections.js +5 -1
- data/assets/js/dist/loader.js +1 -1
- data/assets/js/dist/math.js +2 -1
- data/assets/js/dist/moderation.js +1 -1
- data/assets/js/dist/reactions.js +1 -1
- data/assets/js/dist/search.js +1 -1
- data/assets/js/dist/sources.json +20 -16
- data/assets/js/dist/subscriptions.js +1 -1
- data/assets/js/loader.js +6 -2
- data/lib/datalog/audit/checks.rb +269 -0
- data/lib/datalog/audit/known_keys.rb +61 -0
- data/lib/datalog/audit/site_reader.rb +118 -0
- data/lib/datalog/audit/source_file.rb +85 -0
- data/lib/datalog/audit.rb +145 -0
- data/lib/datalog/citations/bibtex.rb +200 -0
- data/lib/datalog/citations/entry.rb +333 -0
- data/lib/datalog/citations/markup.rb +79 -0
- data/lib/datalog/cli.rb +164 -89
- data/lib/datalog/critical_css.rb +126 -21
- data/lib/datalog/latex_speech/words.json +88 -0
- data/lib/datalog/latex_speech.rb +355 -0
- data/lib/datalog/packages/command.rb +51 -0
- data/lib/datalog/packages/refresh.rb +187 -0
- data/lib/datalog/packages.rb +177 -0
- data/lib/datalog/plugin_system.rb +5 -0
- data/lib/datalog/plugins/citations.rb +325 -109
- data/lib/datalog/site_config.rb +26 -0
- data/lib/datalog/social_cards/font.rb +286 -0
- data/lib/datalog/social_cards/fonts/IBMPlexSans-Regular.ttf +0 -0
- data/lib/datalog/social_cards/fonts/IBMPlexSerif-SemiBold.ttf +0 -0
- data/lib/datalog/social_cards/fonts/OFL.txt +92 -0
- data/lib/datalog/social_cards/geometry.rb +51 -0
- data/lib/datalog/social_cards/outline.rb +54 -0
- data/lib/datalog/social_cards/plain_text.rb +258 -0
- data/lib/datalog/social_cards/template.rb +322 -0
- data/lib/datalog/social_cards/template.svg +17 -0
- data/lib/datalog/social_cards/typesetter.rb +178 -0
- data/lib/datalog/social_cards.rb +369 -0
- data/lib/datalog/theme/updater.rb +307 -0
- data/lib/datalog/theme/version.rb +1 -1
- metadata +44 -7
- data/_includes/components/enhanced-code-block.html +0 -212
- data/_includes/components/performance-monitor.html +0 -170
- data/_includes/components/viz-table-fallback.html +0 -19
- data/_plugins/datalog_bibliography.rb +0 -21
- data/assets/js/dist/chunks/chunk-WIRUK3OZ.js +0 -1
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
{
|
|
2
|
+
"about": "How a LaTeX expression reads as a label for a screen reader (#417). The build (lib/datalog/latex_speech.rb) and the browser (assets/js/math/latex-speech.js) both read this file, so the two say the same; tests/fixtures/latex-speech.json holds the cases both test suites check.",
|
|
3
|
+
"greek": [
|
|
4
|
+
"alpha", "beta", "gamma", "delta", "epsilon", "zeta", "eta", "theta", "iota", "kappa", "lambda", "mu",
|
|
5
|
+
"nu", "xi", "omicron", "pi", "rho", "sigma", "tau", "upsilon", "phi", "chi", "psi", "omega"
|
|
6
|
+
],
|
|
7
|
+
"words": {
|
|
8
|
+
"varepsilon": "epsilon", "vartheta": "theta", "varphi": "phi", "varrho": "rho", "varsigma": "sigma",
|
|
9
|
+
"varpi": "pi", "varkappa": "kappa",
|
|
10
|
+
|
|
11
|
+
"leq": "less than or equal to", "le": "less than or equal to", "leqslant": "less than or equal to",
|
|
12
|
+
"geq": "greater than or equal to", "ge": "greater than or equal to", "geqslant": "greater than or equal to",
|
|
13
|
+
"neq": "not equal to", "ne": "not equal to", "approx": "approximately equal to",
|
|
14
|
+
"equiv": "equivalent to", "sim": "similar to", "simeq": "similar or equal to", "cong": "congruent to",
|
|
15
|
+
"propto": "proportional to", "ll": "much less than", "gg": "much greater than",
|
|
16
|
+
"in": "in", "notin": "not in", "ni": "contains", "subset": "subset of",
|
|
17
|
+
"subseteq": "subset of or equal to", "supset": "superset of", "supseteq": "superset of or equal to",
|
|
18
|
+
"mid": "given", "parallel": "parallel to", "perp": "perpendicular to",
|
|
19
|
+
|
|
20
|
+
"to": "to", "rightarrow": "to", "longrightarrow": "to", "mapsto": "maps to",
|
|
21
|
+
"leftarrow": "left arrow", "gets": "gets", "Rightarrow": "implies", "implies": "implies",
|
|
22
|
+
"Longrightarrow": "implies", "Leftarrow": "is implied by", "impliedby": "is implied by",
|
|
23
|
+
"Leftrightarrow": "if and only if", "iff": "if and only if", "leftrightarrow": "left right arrow",
|
|
24
|
+
"uparrow": "up arrow", "downarrow": "down arrow",
|
|
25
|
+
|
|
26
|
+
"times": "times", "cdot": "dot", "div": "divided by", "pm": "plus or minus", "mp": "minus or plus",
|
|
27
|
+
"ast": "star", "star": "star", "circ": "circle", "bullet": "bullet", "oplus": "direct sum",
|
|
28
|
+
"otimes": "tensor product", "cup": "union", "cap": "intersection", "setminus": "minus",
|
|
29
|
+
"wedge": "and", "land": "and", "vee": "or", "lor": "or", "neg": "not", "lnot": "not",
|
|
30
|
+
"forall": "for all", "exists": "there exists", "nexists": "there does not exist",
|
|
31
|
+
|
|
32
|
+
"infty": "infinity", "partial": "partial", "nabla": "nabla", "emptyset": "empty set",
|
|
33
|
+
"varnothing": "empty set", "prime": "prime", "angle": "angle", "triangle": "triangle",
|
|
34
|
+
"square": "square", "Box": "square", "blacksquare": "end of proof", "qed": "end of proof",
|
|
35
|
+
"hbar": "h bar", "ell": "ell", "aleph": "aleph", "dagger": "dagger",
|
|
36
|
+
"ldots": "dots", "cdots": "dots", "dots": "dots", "vdots": "dots", "ddots": "dots",
|
|
37
|
+
|
|
38
|
+
"langle": "left angle bracket", "rangle": "right angle bracket", "lvert": "|", "rvert": "|",
|
|
39
|
+
"vert": "|", "lVert": "double bar", "rVert": "double bar", "Vert": "double bar", "|": "double bar",
|
|
40
|
+
"lfloor": "floor of", "rfloor": "", "lceil": "ceiling of", "rceil": "",
|
|
41
|
+
"{": "{", "}": "}", "%": "percent", "$": "$", "&": "and", "#": "#", "_": "_", "colon": ":",
|
|
42
|
+
|
|
43
|
+
"newline": ";", ",": "", ";": "", ":": "", "!": "", " ": "", "quad": "", "qquad": "",
|
|
44
|
+
"enspace": "", "thinspace": "", "medspace": "", "thickspace": "", "negthinspace": "",
|
|
45
|
+
"negmedspace": "", "negthickspace": "",
|
|
46
|
+
|
|
47
|
+
"displaystyle": "", "textstyle": "", "scriptstyle": "", "scriptscriptstyle": "", "limits": "",
|
|
48
|
+
"nolimits": "", "nonumber": "", "notag": "", "allowbreak": "", "hline": "", "toprule": "",
|
|
49
|
+
"midrule": "", "bottomrule": ""
|
|
50
|
+
},
|
|
51
|
+
"big_operators": {
|
|
52
|
+
"sum": "summation", "prod": "product", "coprod": "coproduct", "bigcup": "union",
|
|
53
|
+
"bigcap": "intersection", "bigoplus": "direct sum", "bigotimes": "tensor product",
|
|
54
|
+
"int": "integral", "iint": "double integral", "iiint": "triple integral", "oint": "contour integral",
|
|
55
|
+
"max": "max", "min": "min", "sup": "sup", "inf": "inf", "argmax": "argmax", "argmin": "argmin"
|
|
56
|
+
},
|
|
57
|
+
"limits": { "lim": "limit", "limsup": "limit superior", "liminf": "limit inferior" },
|
|
58
|
+
"text": [
|
|
59
|
+
"text", "textrm", "textit", "textbf", "textsf", "texttt", "textnormal", "emph", "mbox", "hbox",
|
|
60
|
+
"mathrm", "mathit", "mathsf", "mathtt", "mathnormal", "operatorname", "boxed"
|
|
61
|
+
],
|
|
62
|
+
"text_mode": [
|
|
63
|
+
"text", "textrm", "textit", "textbf", "textsf", "texttt", "textnormal", "emph", "mbox", "hbox"
|
|
64
|
+
],
|
|
65
|
+
"styles": {
|
|
66
|
+
"mathbb": "double-struck", "mathcal": "script", "mathscr": "script", "mathfrak": "fraktur",
|
|
67
|
+
"mathbf": "bold", "boldsymbol": "bold", "bm": "bold", "pmb": "bold",
|
|
68
|
+
"vec": "vector", "overrightarrow": "vector", "cancel": "cancelled", "bcancel": "cancelled",
|
|
69
|
+
"xcancel": "cancelled", "sout": "cancelled"
|
|
70
|
+
},
|
|
71
|
+
"accents": {
|
|
72
|
+
"hat": "hat", "widehat": "hat", "bar": "bar", "overline": "bar", "tilde": "tilde",
|
|
73
|
+
"widetilde": "tilde", "dot": "dot", "ddot": "double dot", "underline": "underline",
|
|
74
|
+
"check": "check", "breve": "breve", "acute": "acute", "grave": "grave"
|
|
75
|
+
},
|
|
76
|
+
"silent_with_argument": [
|
|
77
|
+
"label", "tag", "hspace", "vspace", "phantom", "hphantom", "vphantom", "color", "eqref", "ref"
|
|
78
|
+
],
|
|
79
|
+
"sizes": [
|
|
80
|
+
"left", "right", "middle", "big", "Big", "bigg", "Bigg", "bigl", "bigr", "Bigl", "Bigr", "biggl", "biggr",
|
|
81
|
+
"Biggl", "Biggr"
|
|
82
|
+
],
|
|
83
|
+
"matrices": ["matrix", "pmatrix", "bmatrix", "Bmatrix", "vmatrix", "Vmatrix", "smallmatrix"],
|
|
84
|
+
"with_columns": ["array", "tabular", "alignat", "alignedat"],
|
|
85
|
+
"superscripts": { "2": "squared", "3": "cubed", "T": "transpose", "-1": "inverse", "prime": "prime" },
|
|
86
|
+
"primes": ["", "prime", "double prime", "triple prime"],
|
|
87
|
+
"roots": { "2": "square root", "3": "cube root" }
|
|
88
|
+
}
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Datalog
|
|
6
|
+
# Reads a LaTeX expression as words, for the label a screen reader announces.
|
|
7
|
+
# Symbols are spoken as words, since a screen reader may skip "∈" or "ℝ" at
|
|
8
|
+
# its usual settings. A command is read, or left out of the name when it
|
|
9
|
+
# isn't part of what the expression says (spacing, \label, a \tag's number),
|
|
10
|
+
# but never lost because no table knows it: an unknown one is read by its
|
|
11
|
+
# name (#417).
|
|
12
|
+
#
|
|
13
|
+
# The browser reads expressions the same way (assets/js/math/latex-speech.js),
|
|
14
|
+
# from the same table, latex_speech/words.json, and both test suites check
|
|
15
|
+
# tests/fixtures/latex-speech.json.
|
|
16
|
+
module LatexSpeech
|
|
17
|
+
TABLE = JSON.parse(File.read(File.expand_path("latex_speech/words.json", __dir__))).freeze
|
|
18
|
+
WORDS = TABLE["greek"].to_h { |name| [name, name] }
|
|
19
|
+
.merge(TABLE["greek"].to_h { |name| ["#{name[0].upcase}#{name[1..]}", "capital #{name}"] })
|
|
20
|
+
.merge(TABLE["words"]).freeze
|
|
21
|
+
BIG_OPERATORS = TABLE["big_operators"].freeze
|
|
22
|
+
LIMITS = TABLE["limits"].freeze
|
|
23
|
+
TEXT = TABLE["text"].to_set.freeze
|
|
24
|
+
TEXT_MODE = TABLE["text_mode"].to_set.freeze
|
|
25
|
+
STYLES = TABLE["styles"].freeze
|
|
26
|
+
ACCENTS = TABLE["accents"].freeze
|
|
27
|
+
SILENT_WITH_ARGUMENT = TABLE["silent_with_argument"].to_set.freeze
|
|
28
|
+
SIZES = TABLE["sizes"].to_set.freeze
|
|
29
|
+
MATRICES = TABLE["matrices"].to_set.freeze
|
|
30
|
+
WITH_COLUMNS = TABLE["with_columns"].to_set.freeze
|
|
31
|
+
MAX_DEPTH = 64
|
|
32
|
+
|
|
33
|
+
module_function
|
|
34
|
+
|
|
35
|
+
# The words a LaTeX expression reads as, or "" when it has none.
|
|
36
|
+
def speak(latex)
|
|
37
|
+
# Single spaces, none inside brackets or before punctuation. After the
|
|
38
|
+
# collapse each pattern matches a fixed character or two, so none
|
|
39
|
+
# backtracks.
|
|
40
|
+
words = Reader.new(tokenize(latex), depth: 0, environments: []).read
|
|
41
|
+
.gsub(/[[:space:]]+/, " ")
|
|
42
|
+
.gsub(/ ([,;:.!?)\]}])/, '\1')
|
|
43
|
+
.gsub(/([(\[{]) /, '\1')
|
|
44
|
+
.gsub(/;(?=;)/, "")
|
|
45
|
+
# A line break or a separator at either end reads as nothing.
|
|
46
|
+
edge = [" ", ";", ","]
|
|
47
|
+
chars = words.chars
|
|
48
|
+
chars.shift while edge.include?(chars.first)
|
|
49
|
+
chars.pop while edge.include?(chars.last)
|
|
50
|
+
chars.join
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Splits LaTeX into commands, brace groups and characters. It walks the
|
|
54
|
+
# text once, keeping open groups on a stack rather than recursing, and
|
|
55
|
+
# drops an unescaped % and the rest of its line.
|
|
56
|
+
def tokenize(source)
|
|
57
|
+
chars = source.to_s.chars
|
|
58
|
+
root = []
|
|
59
|
+
stack = [root]
|
|
60
|
+
index = 0
|
|
61
|
+
while index < chars.length
|
|
62
|
+
char = chars[index]
|
|
63
|
+
current = stack.last
|
|
64
|
+
case char
|
|
65
|
+
when "\\"
|
|
66
|
+
token, index = command_token(chars, index)
|
|
67
|
+
current << token if token
|
|
68
|
+
when "{"
|
|
69
|
+
group = { type: :group, tokens: [] }
|
|
70
|
+
current << group
|
|
71
|
+
stack << group[:tokens]
|
|
72
|
+
index += 1
|
|
73
|
+
when "}"
|
|
74
|
+
stack.pop if stack.length > 1
|
|
75
|
+
index += 1
|
|
76
|
+
when "%"
|
|
77
|
+
index += 1 while index < chars.length && chars[index] != "\n"
|
|
78
|
+
else
|
|
79
|
+
if char.match?(/[[:space:]]/)
|
|
80
|
+
current << { type: :space } unless current.last && current.last[:type] == :space
|
|
81
|
+
else
|
|
82
|
+
current << { type: :char, value: char }
|
|
83
|
+
end
|
|
84
|
+
index += 1
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
root
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# The command a backslash at `index` starts: a run of letters, or the one
|
|
91
|
+
# character after it. A backslash that ends the text is none.
|
|
92
|
+
def command_token(chars, index)
|
|
93
|
+
finish = index + 1
|
|
94
|
+
finish += 1 while finish < chars.length && letter?(chars[finish])
|
|
95
|
+
finish += 1 if finish == index + 1 && finish < chars.length
|
|
96
|
+
token = { type: :command, name: chars[(index + 1)...finish].join } if finish > index + 1
|
|
97
|
+
[token, finish]
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def letter?(char)
|
|
101
|
+
char.match?(/\A[a-zA-Z]\z/)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Reads one list of tokens, as the browser's speak() does.
|
|
105
|
+
class Reader
|
|
106
|
+
SPECIAL = {
|
|
107
|
+
"frac" => :fraction, "dfrac" => :fraction, "tfrac" => :fraction, "cfrac" => :fraction,
|
|
108
|
+
"binom" => :binomial, "dbinom" => :binomial, "tbinom" => :binomial, "sqrt" => :root,
|
|
109
|
+
"begin" => :environment_change, "end" => :environment_change, "\\" => :line_break, "pmod" => :pmod,
|
|
110
|
+
"textcolor" => :text_color, "overset" => :marked, "stackrel" => :marked, "underset" => :marked,
|
|
111
|
+
"overbrace" => :brace, "underbrace" => :brace
|
|
112
|
+
}.freeze
|
|
113
|
+
|
|
114
|
+
def initialize(tokens, context)
|
|
115
|
+
@tokens = tokens
|
|
116
|
+
@context = context
|
|
117
|
+
@inner = context.merge(depth: context[:depth] + 1)
|
|
118
|
+
@index = 0
|
|
119
|
+
@out = +""
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def read
|
|
123
|
+
return "" if @context[:depth] > MAX_DEPTH
|
|
124
|
+
|
|
125
|
+
while @index < @tokens.length
|
|
126
|
+
token = @tokens[@index]
|
|
127
|
+
@index += 1
|
|
128
|
+
case token[:type]
|
|
129
|
+
when :space then @out << " "
|
|
130
|
+
when :group then @out << Reader.new(token[:tokens], @inner).read
|
|
131
|
+
when :char then character(token[:value])
|
|
132
|
+
else command(token[:name])
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
@out
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
private
|
|
139
|
+
|
|
140
|
+
def character(char)
|
|
141
|
+
return @out << char if @context[:text]
|
|
142
|
+
|
|
143
|
+
case char
|
|
144
|
+
when "^" then word(superscript(argument))
|
|
145
|
+
when "_" then word("sub #{read_tokens(argument)}")
|
|
146
|
+
when "'" then primes
|
|
147
|
+
when "&" then @out << (MATRICES.include?(current_environment) || current_environment == "cases" ? ", " : " ")
|
|
148
|
+
when "~" then @out << " "
|
|
149
|
+
else @out << char
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def command(name)
|
|
154
|
+
return send(SPECIAL[name], name) if SPECIAL.key?(name)
|
|
155
|
+
return big_operator(name) if BIG_OPERATORS.key?(name)
|
|
156
|
+
return limit(name) if LIMITS.key?(name)
|
|
157
|
+
return word("approaches") if name == "to" && @context[:approaches]
|
|
158
|
+
|
|
159
|
+
family(name)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def family(name)
|
|
163
|
+
if TEXT.include?(name)
|
|
164
|
+
star
|
|
165
|
+
word(read_tokens(argument, @inner.merge(text: TEXT_MODE.include?(name))))
|
|
166
|
+
elsif STYLES.key?(name) then word("#{STYLES[name]} #{read_tokens(argument)}")
|
|
167
|
+
elsif ACCENTS.key?(name) then word("#{read_tokens(argument)} #{ACCENTS[name]}")
|
|
168
|
+
elsif SIZES.include?(name) then @index += 1 if char?(@tokens[@index], ".")
|
|
169
|
+
elsif SILENT_WITH_ARGUMENT.include?(name)
|
|
170
|
+
star
|
|
171
|
+
argument
|
|
172
|
+
elsif WORDS.key?(name) then word(WORDS[name])
|
|
173
|
+
elsif LatexSpeech.letter?(name[0])
|
|
174
|
+
# A function name (\sin, \log) is read as written, and so is a command
|
|
175
|
+
# nothing here knows, rather than lost.
|
|
176
|
+
word(name)
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def fraction(_name)
|
|
181
|
+
top = read_tokens(argument)
|
|
182
|
+
word("#{top} over #{read_tokens(argument)}")
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def binomial(_name)
|
|
186
|
+
top = read_tokens(argument)
|
|
187
|
+
word("#{top} choose #{read_tokens(argument)}")
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def root(_name)
|
|
191
|
+
degree = optional
|
|
192
|
+
order = degree ? read_tokens(degree) : "2"
|
|
193
|
+
word("#{TABLE['roots'][order] || "#{order}th root"} of #{read_tokens(argument)}")
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def environment_change(name)
|
|
197
|
+
# A starred environment (align*) is the same environment, unnumbered.
|
|
198
|
+
named = argument.filter_map { |token| token[:value] if token[:type] == :char }.join.strip
|
|
199
|
+
environment = named.delete_suffix("*")
|
|
200
|
+
if name == "begin"
|
|
201
|
+
@context[:environments] << environment
|
|
202
|
+
argument if WITH_COLUMNS.include?(environment)
|
|
203
|
+
else
|
|
204
|
+
@context[:environments].pop
|
|
205
|
+
end
|
|
206
|
+
kind = if environment == "cases" then "cases"
|
|
207
|
+
elsif MATRICES.include?(environment) then "matrix"
|
|
208
|
+
end
|
|
209
|
+
word(name == "begin" ? kind : "end #{kind}") if kind
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def line_break(_name)
|
|
213
|
+
optional
|
|
214
|
+
word(";")
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def pmod(_name)
|
|
218
|
+
word("mod #{read_tokens(argument)}")
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def text_color(_name)
|
|
222
|
+
argument
|
|
223
|
+
word(read_tokens(argument, @inner.merge(text: true)))
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def marked(name)
|
|
227
|
+
mark = read_tokens(argument)
|
|
228
|
+
word("#{read_tokens(argument)} with #{mark} #{name == 'underset' ? 'below' : 'above'}")
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# The script after a brace is its label, not a power or an index:
|
|
232
|
+
# \overbrace{a+b}^{n} reads "a+b with n above".
|
|
233
|
+
def brace(name)
|
|
234
|
+
base = read_tokens(argument)
|
|
235
|
+
side = name == "overbrace" ? "^" : "_"
|
|
236
|
+
skip_spaces
|
|
237
|
+
return word(base) unless char?(@tokens[@index], side)
|
|
238
|
+
|
|
239
|
+
@index += 1
|
|
240
|
+
word("#{base} with #{read_tokens(argument)} #{side == '^' ? 'above' : 'below'}")
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def big_operator(name)
|
|
244
|
+
found = limits(@inner)
|
|
245
|
+
lower = found[:lower]
|
|
246
|
+
upper = found[:upper]
|
|
247
|
+
phrase = BIG_OPERATORS[name]
|
|
248
|
+
phrase += if lower && !lower.empty? && upper && !upper.empty? then " from #{lower} to #{upper}"
|
|
249
|
+
elsif lower && !lower.empty? then " over #{lower}"
|
|
250
|
+
elsif upper && !upper.empty? then " to #{upper}"
|
|
251
|
+
else ""
|
|
252
|
+
end
|
|
253
|
+
word(phrase)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def limit(name)
|
|
257
|
+
found = limits(@inner.merge(approaches: true))
|
|
258
|
+
parts = [LIMITS[name]]
|
|
259
|
+
parts << "as #{found[:lower]}" if found[:lower] && !found[:lower].empty?
|
|
260
|
+
parts << "to the power #{found[:upper]}" if found[:upper] && !found[:upper].empty?
|
|
261
|
+
word(parts.join(" "))
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# The sub- and superscript after an operator, in either order.
|
|
265
|
+
def limits(lower_context)
|
|
266
|
+
found = {}
|
|
267
|
+
loop do
|
|
268
|
+
skip_spaces
|
|
269
|
+
token = @tokens[@index]
|
|
270
|
+
if command?(token, "limits") || command?(token, "nolimits")
|
|
271
|
+
@index += 1
|
|
272
|
+
elsif char?(token, "_") && !found.key?(:lower)
|
|
273
|
+
@index += 1
|
|
274
|
+
found[:lower] = read_tokens(argument, lower_context)
|
|
275
|
+
elsif char?(token, "^") && !found.key?(:upper)
|
|
276
|
+
@index += 1
|
|
277
|
+
found[:upper] = read_tokens(argument)
|
|
278
|
+
else
|
|
279
|
+
return found
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def superscript(raised)
|
|
285
|
+
single = raised.length == 1 ? raised.first : nil
|
|
286
|
+
return "degrees" if command?(single, "circ")
|
|
287
|
+
return "transpose" if command?(single, "top")
|
|
288
|
+
|
|
289
|
+
content = read_tokens(raised)
|
|
290
|
+
TABLE["superscripts"].fetch(content) { "to the power #{content}" }
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def primes
|
|
294
|
+
count = 1
|
|
295
|
+
while char?(@tokens[@index], "'")
|
|
296
|
+
count += 1
|
|
297
|
+
@index += 1
|
|
298
|
+
end
|
|
299
|
+
word(TABLE["primes"][count] || Array.new(count, "prime").join(" "))
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def current_environment
|
|
303
|
+
@context[:environments].last
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def word(value)
|
|
307
|
+
@out << " #{value} " if value && !value.empty?
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def skip_spaces
|
|
311
|
+
@index += 1 while @tokens[@index] && @tokens[@index][:type] == :space
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# The next argument: a group's tokens, or a single token.
|
|
315
|
+
def argument
|
|
316
|
+
skip_spaces
|
|
317
|
+
token = @tokens[@index]
|
|
318
|
+
return [] unless token
|
|
319
|
+
|
|
320
|
+
@index += 1
|
|
321
|
+
token[:type] == :group ? token[:tokens] : [token]
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def optional
|
|
325
|
+
skip_spaces
|
|
326
|
+
return nil unless char?(@tokens[@index], "[")
|
|
327
|
+
|
|
328
|
+
collected = []
|
|
329
|
+
@index += 1
|
|
330
|
+
while @index < @tokens.length && !char?(@tokens[@index], "]")
|
|
331
|
+
collected << @tokens[@index]
|
|
332
|
+
@index += 1
|
|
333
|
+
end
|
|
334
|
+
@index += 1
|
|
335
|
+
collected
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def star
|
|
339
|
+
@index += 1 if char?(@tokens[@index], "*")
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def read_tokens(tokens, context = @inner)
|
|
343
|
+
Reader.new(tokens, context).read.strip
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def char?(token, value)
|
|
347
|
+
token && token[:type] == :char && token[:value] == value
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def command?(token, name)
|
|
351
|
+
token && token[:type] == :command && token[:name] == name
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "thor"
|
|
4
|
+
|
|
5
|
+
module Datalog
|
|
6
|
+
# `datalog packages`: commands about the site's package pages.
|
|
7
|
+
class PackagesCommand < Thor
|
|
8
|
+
class_option :root, type: :string, default: Dir.pwd, aliases: "-r",
|
|
9
|
+
desc: "Path to the Jekyll site using the DataLog theme"
|
|
10
|
+
|
|
11
|
+
desc "refresh", "Write each package's latest release, from its registries, to _data/package_releases.yml"
|
|
12
|
+
method_option :dry_run, type: :boolean, default: false, desc: "Print the file instead of writing it"
|
|
13
|
+
long_desc <<~DESC
|
|
14
|
+
Reads the latest release of every package in the packages collection from
|
|
15
|
+
each registry its `registry:` front matter names (PyPI, crates.io, RubyGems,
|
|
16
|
+
CRAN, npm): version, release date, licence, the language version it needs,
|
|
17
|
+
and whether it is a pre-release or yanked. The package layout shows these
|
|
18
|
+
over the front matter, and the build itself never goes to a registry.
|
|
19
|
+
If a registry cannot be read, nothing is written and the command exits 1.
|
|
20
|
+
DESC
|
|
21
|
+
def refresh
|
|
22
|
+
require_relative "refresh"
|
|
23
|
+
refresher = new_refresher
|
|
24
|
+
data, yaml = refresher.run(dry_run: options[:dry_run])
|
|
25
|
+
file = refresher.data_file.delete_prefix("#{refresher.root}/")
|
|
26
|
+
count = data.values.sum(&:size)
|
|
27
|
+
if count.zero?
|
|
28
|
+
say "No package names a registry this command reads (#{Packages::ENDPOINTS.keys.join(', ')}); " \
|
|
29
|
+
"#{file} was left as it was."
|
|
30
|
+
elsif options[:dry_run]
|
|
31
|
+
say yaml
|
|
32
|
+
else
|
|
33
|
+
say "Wrote #{count} #{count == 1 ? 'release' : 'releases'} to #{file}"
|
|
34
|
+
end
|
|
35
|
+
rescue Packages::Refresh::Error => e
|
|
36
|
+
say e.message, :red
|
|
37
|
+
exit 1
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
no_commands do
|
|
41
|
+
# What reads the registries; a test hands it a fetch that reads fixtures.
|
|
42
|
+
def new_refresher
|
|
43
|
+
Packages::Refresh.new(root: options[:root])
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def self.exit_on_failure?
|
|
48
|
+
true
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|