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.
Files changed (130) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +127 -0
  3. data/CITATION.cff +2 -2
  4. data/README.md +4 -2
  5. data/_data/i18n/en.yml +55 -3
  6. data/_data/i18n/es.yml +55 -3
  7. data/_data/i18n/pt.yml +55 -3
  8. data/_includes/analytics/dashboard.html +24 -9
  9. data/_includes/components/academic-dashboard.html +168 -133
  10. data/_includes/components/author-bio.html +7 -5
  11. data/_includes/components/author-list.html +6 -5
  12. data/_includes/components/citation-tools.html +2 -1
  13. data/_includes/components/content-provenance.html +19 -1
  14. data/_includes/components/correction-report.html +15 -8
  15. data/_includes/components/hero.html +22 -15
  16. data/_includes/components/package-index.html +121 -0
  17. data/_includes/components/package-install.html +27 -50
  18. data/_includes/components/post-list.html +14 -0
  19. data/_includes/components/responsive-image.html +15 -21
  20. data/_includes/footer.html +1 -1
  21. data/_includes/head.html +70 -11
  22. data/_includes/layouts/default/article.html +76 -46
  23. data/_includes/meta/dataset-json.html +171 -0
  24. data/_includes/meta/math-config.html +15 -1
  25. data/_includes/meta/package-json.html +69 -0
  26. data/_includes/meta/person-json.html +36 -13
  27. data/_includes/meta/publisher.html +15 -0
  28. data/_includes/meta/schema.html +108 -23
  29. data/_includes/meta/scholarly.html +10 -5
  30. data/_includes/post/related-posts.html +17 -43
  31. data/_includes/scripts.html +2 -2
  32. data/_includes/search/index-data.json +10 -1
  33. data/_includes/search/page.html +11 -6
  34. data/_layouts/archive.html +67 -0
  35. data/_layouts/dataset.html +2 -1
  36. data/_layouts/default.html +13 -11
  37. data/_layouts/docs.html +53 -0
  38. data/_layouts/home.html +30 -1
  39. data/_layouts/notebook.html +5 -1
  40. data/_layouts/package.html +38 -21
  41. data/_layouts/portfolio.html +1 -0
  42. data/_layouts/post.html +21 -62
  43. data/_layouts/project.html +2 -1
  44. data/_layouts/research.html +14 -2
  45. data/_plugins/analytics_dashboard.rb +4 -1
  46. data/_plugins/archive.rb +114 -0
  47. data/_plugins/authors.rb +50 -1
  48. data/_plugins/citation_exports.rb +12 -1
  49. data/_plugins/config_validator.rb +77 -2
  50. data/_plugins/correction_fallback.rb +110 -0
  51. data/_plugins/image_optimizer.rb +129 -22
  52. data/_plugins/math_preprocessor.rb +7 -48
  53. data/_plugins/notebook_converter.rb +44 -5
  54. data/_plugins/packages.rb +84 -0
  55. data/_plugins/page_dates.rb +37 -0
  56. data/_plugins/references.rb +16 -2
  57. data/_plugins/related_posts.rb +149 -0
  58. data/_plugins/search_sections.rb +110 -0
  59. data/_plugins/site_identity.rb +42 -0
  60. data/_plugins/social_cards.rb +17 -0
  61. data/_sass/_academic-dashboard.scss +18 -30
  62. data/_sass/_base.scss +2 -0
  63. data/_sass/_components.scss +21 -23
  64. data/_sass/_docs.scss +143 -0
  65. data/_sass/_features.scss +6 -0
  66. data/_sass/_layout.scss +65 -6
  67. data/_sass/_mathematical.scss +61 -19
  68. data/_sass/_notebooks.scss +1 -32
  69. data/_sass/_package-docs.scss +6 -9
  70. data/_sass/_post-components.scss +23 -23
  71. data/_sass/_print.scss +1 -1
  72. data/_sass/_search-page.scss +75 -0
  73. data/_sass/_search.scss +17 -26
  74. data/_sass/_syntax-highlighting.scss +23 -1
  75. data/_sass/_theme.scss +1 -0
  76. data/_sass/_utilities.scss +87 -40
  77. data/_sass/_variables.scss +31 -29
  78. data/assets/css/main.scss +2 -1
  79. data/assets/js/dist/academic.js +1 -1
  80. data/assets/js/dist/analytics-dashboard.js +1 -1
  81. data/assets/js/dist/chunks/chunk-TNVD6UAM.js +1 -0
  82. data/assets/js/dist/comments.js +1 -1
  83. data/assets/js/dist/contact.js +1 -1
  84. data/assets/js/dist/core.js +1 -1
  85. data/assets/js/dist/corrections.js +5 -1
  86. data/assets/js/dist/loader.js +1 -1
  87. data/assets/js/dist/math.js +2 -1
  88. data/assets/js/dist/moderation.js +1 -1
  89. data/assets/js/dist/reactions.js +1 -1
  90. data/assets/js/dist/search.js +1 -1
  91. data/assets/js/dist/sources.json +20 -16
  92. data/assets/js/dist/subscriptions.js +1 -1
  93. data/assets/js/loader.js +6 -2
  94. data/lib/datalog/audit/checks.rb +269 -0
  95. data/lib/datalog/audit/known_keys.rb +61 -0
  96. data/lib/datalog/audit/site_reader.rb +118 -0
  97. data/lib/datalog/audit/source_file.rb +85 -0
  98. data/lib/datalog/audit.rb +145 -0
  99. data/lib/datalog/citations/bibtex.rb +200 -0
  100. data/lib/datalog/citations/entry.rb +333 -0
  101. data/lib/datalog/citations/markup.rb +79 -0
  102. data/lib/datalog/cli.rb +164 -89
  103. data/lib/datalog/critical_css.rb +126 -21
  104. data/lib/datalog/latex_speech/words.json +88 -0
  105. data/lib/datalog/latex_speech.rb +355 -0
  106. data/lib/datalog/packages/command.rb +51 -0
  107. data/lib/datalog/packages/refresh.rb +187 -0
  108. data/lib/datalog/packages.rb +177 -0
  109. data/lib/datalog/plugin_system.rb +5 -0
  110. data/lib/datalog/plugins/citations.rb +325 -109
  111. data/lib/datalog/site_config.rb +26 -0
  112. data/lib/datalog/social_cards/font.rb +286 -0
  113. data/lib/datalog/social_cards/fonts/IBMPlexSans-Regular.ttf +0 -0
  114. data/lib/datalog/social_cards/fonts/IBMPlexSerif-SemiBold.ttf +0 -0
  115. data/lib/datalog/social_cards/fonts/OFL.txt +92 -0
  116. data/lib/datalog/social_cards/geometry.rb +51 -0
  117. data/lib/datalog/social_cards/outline.rb +54 -0
  118. data/lib/datalog/social_cards/plain_text.rb +258 -0
  119. data/lib/datalog/social_cards/template.rb +322 -0
  120. data/lib/datalog/social_cards/template.svg +17 -0
  121. data/lib/datalog/social_cards/typesetter.rb +178 -0
  122. data/lib/datalog/social_cards.rb +369 -0
  123. data/lib/datalog/theme/updater.rb +307 -0
  124. data/lib/datalog/theme/version.rb +1 -1
  125. metadata +44 -7
  126. data/_includes/components/enhanced-code-block.html +0 -212
  127. data/_includes/components/performance-monitor.html +0 -170
  128. data/_includes/components/viz-table-fallback.html +0 -19
  129. data/_plugins/datalog_bibliography.rb +0 -21
  130. 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