jekyll-theme-apcsp 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (183) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/_includes/alert.md +0 -0
  4. data/_includes/footer.md +0 -0
  5. data/_includes/head.html +0 -0
  6. data/_includes/header.md +1 -0
  7. data/_includes/nav.md +0 -0
  8. data/_layouts/page.html +308 -0
  9. data/_layouts/redirect.html +12 -0
  10. data/_sass/bootstrap/LICENSE +22 -0
  11. data/_sass/bootstrap/scss/_accordion.scss +146 -0
  12. data/_sass/bootstrap/scss/_alert.scss +71 -0
  13. data/_sass/bootstrap/scss/_badge.scss +40 -0
  14. data/_sass/bootstrap/scss/_breadcrumb.scss +42 -0
  15. data/_sass/bootstrap/scss/_button-group.scss +142 -0
  16. data/_sass/bootstrap/scss/_buttons.scss +158 -0
  17. data/_sass/bootstrap/scss/_card.scss +234 -0
  18. data/_sass/bootstrap/scss/_carousel.scss +229 -0
  19. data/_sass/bootstrap/scss/_close.scss +40 -0
  20. data/_sass/bootstrap/scss/_code.scss +48 -0
  21. data/_sass/bootstrap/scss/_containers.scss +41 -0
  22. data/_sass/bootstrap/scss/_custom-forms.scss +526 -0
  23. data/_sass/bootstrap/scss/_dropdown.scss +248 -0
  24. data/_sass/bootstrap/scss/_forms.scss +9 -0
  25. data/_sass/bootstrap/scss/_functions.scss +302 -0
  26. data/_sass/bootstrap/scss/_grid.scss +33 -0
  27. data/_sass/bootstrap/scss/_helpers.scss +10 -0
  28. data/_sass/bootstrap/scss/_images.scss +42 -0
  29. data/_sass/bootstrap/scss/_input-group.scss +208 -0
  30. data/_sass/bootstrap/scss/_jumbotron.scss +17 -0
  31. data/_sass/bootstrap/scss/_list-group.scss +191 -0
  32. data/_sass/bootstrap/scss/_maps.scss +54 -0
  33. data/_sass/bootstrap/scss/_media.scss +8 -0
  34. data/_sass/bootstrap/scss/_mixins.scss +43 -0
  35. data/_sass/bootstrap/scss/_modal.scss +237 -0
  36. data/_sass/bootstrap/scss/_nav.scss +172 -0
  37. data/_sass/bootstrap/scss/_navbar.scss +274 -0
  38. data/_sass/bootstrap/scss/_offcanvas.scss +143 -0
  39. data/_sass/bootstrap/scss/_pagination.scss +111 -0
  40. data/_sass/bootstrap/scss/_placeholders.scss +51 -0
  41. data/_sass/bootstrap/scss/_popover.scss +198 -0
  42. data/_sass/bootstrap/scss/_print.scss +141 -0
  43. data/_sass/bootstrap/scss/_progress.scss +59 -0
  44. data/_sass/bootstrap/scss/_reboot.scss +610 -0
  45. data/_sass/bootstrap/scss/_root.scss +76 -0
  46. data/_sass/bootstrap/scss/_spinners.scss +85 -0
  47. data/_sass/bootstrap/scss/_tables.scss +164 -0
  48. data/_sass/bootstrap/scss/_toasts.scss +70 -0
  49. data/_sass/bootstrap/scss/_tooltip.scss +122 -0
  50. data/_sass/bootstrap/scss/_transitions.scss +27 -0
  51. data/_sass/bootstrap/scss/_type.scss +104 -0
  52. data/_sass/bootstrap/scss/_utilities.scss +647 -0
  53. data/_sass/bootstrap/scss/_variables.scss +1631 -0
  54. data/_sass/bootstrap/scss/bootstrap-grid.scss +68 -0
  55. data/_sass/bootstrap/scss/bootstrap-reboot.scss +14 -0
  56. data/_sass/bootstrap/scss/bootstrap-utilities.scss +19 -0
  57. data/_sass/bootstrap/scss/bootstrap.scss +54 -0
  58. data/_sass/bootstrap/scss/forms/_floating-labels.scss +70 -0
  59. data/_sass/bootstrap/scss/forms/_form-check.scss +175 -0
  60. data/_sass/bootstrap/scss/forms/_form-control.scss +189 -0
  61. data/_sass/bootstrap/scss/forms/_form-range.scss +91 -0
  62. data/_sass/bootstrap/scss/forms/_form-select.scss +71 -0
  63. data/_sass/bootstrap/scss/forms/_form-text.scss +11 -0
  64. data/_sass/bootstrap/scss/forms/_input-group.scss +121 -0
  65. data/_sass/bootstrap/scss/forms/_labels.scss +36 -0
  66. data/_sass/bootstrap/scss/forms/_validation.scss +12 -0
  67. data/_sass/bootstrap/scss/helpers/_clearfix.scss +3 -0
  68. data/_sass/bootstrap/scss/helpers/_color-bg.scss +10 -0
  69. data/_sass/bootstrap/scss/helpers/_colored-links.scss +12 -0
  70. data/_sass/bootstrap/scss/helpers/_position.scss +36 -0
  71. data/_sass/bootstrap/scss/helpers/_ratio.scss +26 -0
  72. data/_sass/bootstrap/scss/helpers/_stacks.scss +15 -0
  73. data/_sass/bootstrap/scss/helpers/_stretched-link.scss +15 -0
  74. data/_sass/bootstrap/scss/helpers/_text-truncation.scss +7 -0
  75. data/_sass/bootstrap/scss/helpers/_visually-hidden.scss +8 -0
  76. data/_sass/bootstrap/scss/helpers/_vr.scss +8 -0
  77. data/_sass/bootstrap/scss/mixins/_alert.scss +15 -0
  78. data/_sass/bootstrap/scss/mixins/_backdrop.scss +14 -0
  79. data/_sass/bootstrap/scss/mixins/_background-variant.scss +23 -0
  80. data/_sass/bootstrap/scss/mixins/_badge.scss +17 -0
  81. data/_sass/bootstrap/scss/mixins/_border-radius.scss +78 -0
  82. data/_sass/bootstrap/scss/mixins/_box-shadow.scss +18 -0
  83. data/_sass/bootstrap/scss/mixins/_breakpoints.scss +127 -0
  84. data/_sass/bootstrap/scss/mixins/_buttons.scss +71 -0
  85. data/_sass/bootstrap/scss/mixins/_caret.scss +64 -0
  86. data/_sass/bootstrap/scss/mixins/_clearfix.scss +9 -0
  87. data/_sass/bootstrap/scss/mixins/_color-scheme.scss +7 -0
  88. data/_sass/bootstrap/scss/mixins/_container.scss +11 -0
  89. data/_sass/bootstrap/scss/mixins/_deprecate.scss +10 -0
  90. data/_sass/bootstrap/scss/mixins/_float.scss +14 -0
  91. data/_sass/bootstrap/scss/mixins/_forms.scss +152 -0
  92. data/_sass/bootstrap/scss/mixins/_gradients.scss +47 -0
  93. data/_sass/bootstrap/scss/mixins/_grid-framework.scss +80 -0
  94. data/_sass/bootstrap/scss/mixins/_grid.scss +151 -0
  95. data/_sass/bootstrap/scss/mixins/_hover.scss +37 -0
  96. data/_sass/bootstrap/scss/mixins/_image.scss +16 -0
  97. data/_sass/bootstrap/scss/mixins/_list-group.scss +24 -0
  98. data/_sass/bootstrap/scss/mixins/_lists.scss +7 -0
  99. data/_sass/bootstrap/scss/mixins/_nav-divider.scss +11 -0
  100. data/_sass/bootstrap/scss/mixins/_pagination.scss +10 -0
  101. data/_sass/bootstrap/scss/mixins/_reset-text.scss +17 -0
  102. data/_sass/bootstrap/scss/mixins/_resize.scss +6 -0
  103. data/_sass/bootstrap/scss/mixins/_screen-reader.scss +34 -0
  104. data/_sass/bootstrap/scss/mixins/_size.scss +7 -0
  105. data/_sass/bootstrap/scss/mixins/_table-row.scss +39 -0
  106. data/_sass/bootstrap/scss/mixins/_table-variants.scss +24 -0
  107. data/_sass/bootstrap/scss/mixins/_text-emphasis.scss +17 -0
  108. data/_sass/bootstrap/scss/mixins/_text-hide.scss +11 -0
  109. data/_sass/bootstrap/scss/mixins/_text-truncate.scss +8 -0
  110. data/_sass/bootstrap/scss/mixins/_transition.scss +26 -0
  111. data/_sass/bootstrap/scss/mixins/_utilities.scss +92 -0
  112. data/_sass/bootstrap/scss/mixins/_visibility.scss +8 -0
  113. data/_sass/bootstrap/scss/mixins/_visually-hidden.scss +29 -0
  114. data/_sass/bootstrap/scss/utilities/_align.scss +8 -0
  115. data/_sass/bootstrap/scss/utilities/_api.scss +47 -0
  116. data/_sass/bootstrap/scss/utilities/_background.scss +19 -0
  117. data/_sass/bootstrap/scss/utilities/_borders.scss +75 -0
  118. data/_sass/bootstrap/scss/utilities/_clearfix.scss +3 -0
  119. data/_sass/bootstrap/scss/utilities/_display.scss +26 -0
  120. data/_sass/bootstrap/scss/utilities/_embed.scss +39 -0
  121. data/_sass/bootstrap/scss/utilities/_flex.scss +51 -0
  122. data/_sass/bootstrap/scss/utilities/_float.scss +11 -0
  123. data/_sass/bootstrap/scss/utilities/_interactions.scss +5 -0
  124. data/_sass/bootstrap/scss/utilities/_overflow.scss +5 -0
  125. data/_sass/bootstrap/scss/utilities/_position.scss +32 -0
  126. data/_sass/bootstrap/scss/utilities/_screenreaders.scss +11 -0
  127. data/_sass/bootstrap/scss/utilities/_shadows.scss +6 -0
  128. data/_sass/bootstrap/scss/utilities/_sizing.scss +20 -0
  129. data/_sass/bootstrap/scss/utilities/_spacing.scss +73 -0
  130. data/_sass/bootstrap/scss/utilities/_stretched-link.scss +19 -0
  131. data/_sass/bootstrap/scss/utilities/_text.scss +72 -0
  132. data/_sass/bootstrap/scss/utilities/_visibility.scss +13 -0
  133. data/_sass/bootstrap/scss/vendor/_rfs.scss +354 -0
  134. data/_sass/jekyll-theme-cs50.scss +298 -0
  135. data/_sass/page.scss +228 -0
  136. data/_sass/rouge.scss +35 -0
  137. data/assets/@fortawesome/fontawesome-free/css/all.min.css +5 -0
  138. data/assets/@fortawesome/fontawesome-free/webfonts/fa-brands-400.eot +0 -0
  139. data/assets/@fortawesome/fontawesome-free/webfonts/fa-brands-400.svg +3717 -0
  140. data/assets/@fortawesome/fontawesome-free/webfonts/fa-brands-400.ttf +0 -0
  141. data/assets/@fortawesome/fontawesome-free/webfonts/fa-brands-400.woff +0 -0
  142. data/assets/@fortawesome/fontawesome-free/webfonts/fa-brands-400.woff2 +0 -0
  143. data/assets/@fortawesome/fontawesome-free/webfonts/fa-regular-400.eot +0 -0
  144. data/assets/@fortawesome/fontawesome-free/webfonts/fa-regular-400.svg +801 -0
  145. data/assets/@fortawesome/fontawesome-free/webfonts/fa-regular-400.ttf +0 -0
  146. data/assets/@fortawesome/fontawesome-free/webfonts/fa-regular-400.woff +0 -0
  147. data/assets/@fortawesome/fontawesome-free/webfonts/fa-regular-400.woff2 +0 -0
  148. data/assets/@fortawesome/fontawesome-free/webfonts/fa-solid-900.eot +0 -0
  149. data/assets/@fortawesome/fontawesome-free/webfonts/fa-solid-900.svg +5034 -0
  150. data/assets/@fortawesome/fontawesome-free/webfonts/fa-solid-900.ttf +0 -0
  151. data/assets/@fortawesome/fontawesome-free/webfonts/fa-solid-900.woff +0 -0
  152. data/assets/@fortawesome/fontawesome-free/webfonts/fa-solid-900.woff2 +0 -0
  153. data/assets/algolia-min.css +1 -0
  154. data/assets/algoliasearch/dist/algoliasearch-lite.umd.js +2 -0
  155. data/assets/algoliasearchLite.min.js +3 -0
  156. data/assets/bootstrap/LICENSE +22 -0
  157. data/assets/bootstrap/dist/js/bootstrap.bundle.min.js +7 -0
  158. data/assets/bootstrap/dist/js/bootstrap.bundle.min.js.map +1 -0
  159. data/assets/bootstrap/dist/js/bootstrap.min.js +7 -0
  160. data/assets/bootstrap-table/LICENSE +21 -0
  161. data/assets/bootstrap-table/dist/bootstrap-table.min.css +10 -0
  162. data/assets/bootstrap-table/dist/bootstrap-table.min.js +10 -0
  163. data/assets/bootstrap-table/dist/extensions/mobile/bootstrap-table-mobile.min.js +10 -0
  164. data/assets/instantsearch.js/LICENSE +21 -0
  165. data/assets/instantsearch.js/dist/instantsearch.production.min.js +3 -0
  166. data/assets/instantsearch.js/dist/instantsearch.production.min.js.map +1 -0
  167. data/assets/instantsearch.production.min.js +3 -0
  168. data/assets/jekyll-theme-cs50.js +620 -0
  169. data/assets/jquery/LICENSE.txt +20 -0
  170. data/assets/jquery/dist/jquery.min.js +2 -0
  171. data/assets/luxon.min.js +1 -0
  172. data/assets/moment-timezone-with-data.min.js +1 -0
  173. data/assets/moment.min.js +1 -0
  174. data/assets/page.js +4 -0
  175. data/assets/page.scss +4 -0
  176. data/assets/popper.js/dist/umd/popper.min.js +5 -0
  177. data/assets/reset-min.css +1 -0
  178. data/assets/scratchblocks.min.js +11 -0
  179. data/assets/scratchblocks.min.js.map +1 -0
  180. data/lib/jekyll-theme-cs50/constants.rb +59 -0
  181. data/lib/jekyll-theme-cs50.rb +606 -0
  182. data/lib/liquid-tag-parser.rb +1 -0
  183. metadata +350 -0
@@ -0,0 +1,59 @@
1
+ module CS50
2
+
3
+ PLUGINS = {
4
+ "jekyll-algolia" => "1.7.1",
5
+ "jekyll-default-layout" => "0.1.5",
6
+ "jekyll-optional-front-matter" => "0.3.2",
7
+ "jekyll-redirect-from" => "0.16.0",
8
+ "jekyll-titles-from-headings" => "0.5.3"
9
+ }.freeze
10
+
11
+ DEFAULTS = {
12
+ "cs50" => {
13
+ "local" => {
14
+ "day" => "numeric",
15
+ "hour" => "numeric",
16
+ "minute" => "numeric",
17
+ "month" => "long",
18
+ "timeZoneName" => "short",
19
+ "weekday" => "long",
20
+ "year" => "numeric"
21
+ },
22
+ "locale" => "en",
23
+ "tz" => "America/New_York"
24
+ },
25
+ "exclude" => [
26
+ "Gemfile",
27
+ "Gemfile.lock",
28
+ "vendor"
29
+ ],
30
+ "include" => [
31
+ "license.md" # For OCW
32
+ ],
33
+ "optional_front_matter" => {
34
+ "remove_originals" => true
35
+ },
36
+ "plugins" => CS50::PLUGINS.keys
37
+ }.freeze
38
+
39
+ OVERRIDES = {
40
+ "kramdown" => {
41
+ "gfm_quirks" => "paragraph_end",
42
+ "hard_wrap" => false,
43
+ "input" => "GFM",
44
+ "math_engine" => "mathjax",
45
+ "syntax_highlighter" => "rouge",
46
+ "template" => ""
47
+ },
48
+ "markdown" => "kramdown",
49
+ "permalink" => "pretty",
50
+ "redirect_from" => {
51
+ "json" => false
52
+ },
53
+ "sass" => {
54
+ "style" => "compressed"
55
+ },
56
+ "theme" => "jekyll-theme-cs50"
57
+ }.freeze
58
+
59
+ end
@@ -0,0 +1,606 @@
1
+ require "cgi"
2
+ require "deep_merge"
3
+ require "digest/md5"
4
+ require "jekyll"
5
+ require "jekyll-redirect-from"
6
+ require "kramdown/parser/gfm"
7
+ require "kramdown/parser/kramdown/link"
8
+ require "pathname"
9
+ require "sanitize"
10
+ require "time"
11
+ require "uri"
12
+
13
+ require "jekyll-theme-cs50/constants"
14
+
15
+ # https://stackoverflow.com/a/68039635/5156190
16
+ class Hash
17
+ def deep_compact
18
+ compact.transform_values{|vl| vl.is_a?(Hash) ? vl.deep_compact : vl }
19
+ end
20
+ end
21
+
22
+ # Inspired by http://www.glitchwrks.com/2017/07/25/jekyll-plugins, https://github.com/Shopify/liquid/wiki/Liquid-for-Programmers#create-your-own-tag-blocks
23
+
24
+ module CS50
25
+
26
+ # Convert Markdown to HTML, preserving indentation (so that it can still be stripped elsewhere in pipeline)
27
+ def self.convert(s)
28
+ markdown, indentation = CS50::unindent(s)
29
+ html = $site.find_converter_instance(::Jekyll::Converters::Markdown).convert(markdown).strip
30
+ CS50::indent(html, indentation)
31
+ end
32
+
33
+ # Format Time for time.cs50.io
34
+ # https://stackoverflow.com/a/19329068/5156190
35
+ def self.format(t)
36
+ t.strftime("%Y%m%dT%H%M%S%z").sub(/\+0000/, "Z")
37
+ end
38
+
39
+ # Indent multiline string
40
+ def self.indent(s, n)
41
+ s.gsub(/^/, " " * n)
42
+ end
43
+
44
+ # Sanitize string, allowing only these tags, which are a (reasonable) subset of
45
+ # https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Content_categories#Phrasing_content
46
+ def self.sanitize(s)
47
+ Sanitize.fragment(s, :elements => ["b", "code", "em", "i", "img", "kbd", "span", "strong", "sub", "sup"]).strip
48
+ end
49
+
50
+ # Parse time
51
+ def self.strptime(s, now = nil)
52
+
53
+ # Try YYYY-MM-DD HH:MM:SS
54
+ begin
55
+ Time.strptime(s, "%Y-%m-%d %H:%M:%S")
56
+ rescue
57
+
58
+ # Try YYYY-MM-DD HH:MM
59
+ begin
60
+ Time.strptime(s, "%Y-%m-%d %H:%M")
61
+ rescue
62
+
63
+ # Try HH:MM:SS, relative to now
64
+ begin
65
+ t = Time.strptime(s, "%H:%M:%S", now)
66
+ rescue
67
+
68
+ # Try HH:MM, relative to now
69
+ begin
70
+ t = Time.strptime(s, "%H:%M", now)
71
+ rescue
72
+ raise "Invalid datetime: #{s}"
73
+ end
74
+ end
75
+
76
+ # Because Time.strptime parses relative to now's date,
77
+ # not now's date plus time, add one day if t is in past
78
+ if t < now
79
+ t += 24 * 60 * 60
80
+ end
81
+ t
82
+ end
83
+ end
84
+ end
85
+
86
+ # Unindent multiline string
87
+ # https://github.com/mynyml/unindent/blob/master/lib/unindent.rb
88
+ def self.unindent(s)
89
+ n = s.split("\n").select {|line| !line.strip.empty? }.map {|line| line.index(/[^\s]/) }.compact.min || 0
90
+ s = s.gsub(/^[[:blank:]]{#{n}}/, "")
91
+ return s, n
92
+ end
93
+
94
+ # https://github.com/pathawks/liquid-md5/blob/master/lib/liquid-md5.rb
95
+ module MDhash
96
+ def md5(input)
97
+ Digest::MD5.hexdigest input.strip
98
+ end
99
+ end
100
+ Liquid::Template.register_filter(MDhash)
101
+
102
+ module Mixins
103
+
104
+ def initialize(tag_name, markup, options)
105
+ @tag_name = tag_name
106
+ @markup = markup
107
+ super
108
+ end
109
+
110
+ def render(context)
111
+
112
+ # Interpolate any variables, a la render_variable in
113
+ # https://github.com/jekyll/jekyll/blob/master/lib/jekyll/tags/include.rb
114
+ output = @markup.gsub(/\{\{.*?\}\}/) do |s|
115
+ Liquid::Template.parse(s).render(context)
116
+ end
117
+
118
+ # Quote unquoted URLs
119
+ output = output.gsub(/(\S+\s*=\s*".*"|\S+\s*=\s*'.*'|".*"|'.*'|\S+)/) do |s|
120
+ if s =~ /^#{URI::regexp}$/
121
+ "\"#{s}\""
122
+ else
123
+ s
124
+ end
125
+ end
126
+
127
+ # Parse any arguments
128
+ @args, @kwargs = [], {}
129
+ output.scan(/"[^"]*"|'[^']'|\S+\s*=\s*"[^"]*"|\S+\s*=\s*'[^']*'|\S+\s*=\s*\S+|\S+/).each do |s|
130
+ if s.start_with?("'")
131
+ @args.push(s.gsub(/^'|'$/, ""))
132
+ elsif s.start_with?('"')
133
+ @args.push(s.gsub(/^"|"$/, ""))
134
+ else
135
+ key , value = s.split("=", 2)
136
+ if value.nil?
137
+ @args.push(key)
138
+ else
139
+ @kwargs[key] = value
140
+ end
141
+ end
142
+ end
143
+
144
+ # Return any content
145
+ super
146
+ end
147
+ end
148
+
149
+ class Tag < Liquid::Tag
150
+ include Mixins
151
+ end
152
+
153
+ class Block < Liquid::Block
154
+ include Mixins
155
+ end
156
+
157
+ class TestTag < Tag
158
+ def render(context)
159
+ super
160
+ puts @args.inspect
161
+ end
162
+ Liquid::Template.register_tag("test", self)
163
+ end
164
+
165
+ class AfterBeforeBlock < Block
166
+
167
+ def render(context)
168
+ html = CS50::convert(super)
169
+
170
+ # Parse timestamp
171
+ iso8601 = CS50::strptime(@args[0]).iso8601
172
+
173
+ # Render HTML
174
+ "<div data-#{@tag_name}='#{iso8601}'>#{html}</div>"
175
+ end
176
+
177
+ Liquid::Template.register_tag("after", self)
178
+ Liquid::Template.register_tag("before", self)
179
+
180
+ end
181
+
182
+ class AlertBlock < Block
183
+
184
+ def render(context)
185
+ html = CS50::convert(super)
186
+ alert = (["primary", "secondary", "success", "danger", "warning", "info", "light", "dark"].include? @args[0]) ? @args[0] : ""
187
+ "<div class='alert' data-alert='#{alert}' role='alert'>" \
188
+ "#{html}" \
189
+ "</div>"
190
+ end
191
+
192
+ Liquid::Template.register_tag("alert", self)
193
+
194
+ end
195
+
196
+ class CalendarTag < Tag
197
+
198
+ # https://gist.github.com/niquepa/4c59b7d52a15dde2367a
199
+ def initialize(tag_name, markup, options)
200
+ super
201
+
202
+ end
203
+
204
+ def render(context)
205
+ super
206
+
207
+ # Deprecated @ctz
208
+ if @kwargs.key?("ctz")
209
+ Jekyll.logger.warn "CS50 warning: no need for @ctz anymore"
210
+ end
211
+
212
+ # Calendar's height
213
+ height = @kwargs["height"] || "480"
214
+
215
+ # Default components
216
+ components = {
217
+ height: height,
218
+ hl: "en_US",
219
+ mode: @kwargs["mode"] || "AGENDA",
220
+ showCalendars: "0",
221
+ showDate: "0",
222
+ showNav: "0",
223
+ showPrint: "0",
224
+ showTabs: "0",
225
+ showTitle: "0",
226
+ showTz: "1",
227
+ src: @args[0]
228
+ }
229
+
230
+ # Build URL
231
+ src = URI::HTTPS.build(:host => "calendar.google.com", :path => "/calendar/embed", :query => URI.encode_www_form(components))
232
+
233
+ # Render HTML
234
+ "<iframe data-calendar='#{src}' #{@kwargs['ctz'] ? 'data-ctz' : ''} style='height: #{height}px;'></iframe>"
235
+ end
236
+
237
+ Liquid::Template.register_tag("calendar", self)
238
+
239
+ end
240
+
241
+ class LocalTag < Tag
242
+
243
+ @@regex = "\d{4}-\d{2}-\d{2} \d{1,2}:\d{2}(:\d{2})?"
244
+
245
+ def render(context)
246
+ super
247
+
248
+ # Parse required argument
249
+ if @args.length < 1
250
+ raise "Too few arguments"
251
+ elsif @args.length > 2
252
+ raise "Too many arguments: #{@markup}"
253
+ end
254
+ t1 = CS50::strptime(@args[0])
255
+ local = t1.iso8601
256
+ path = CS50::format(t1)
257
+
258
+ # Parse optional argument
259
+ if @args.length == 2
260
+ t2 = CS50::strptime(@args[1], t1)
261
+ if t2 < t1
262
+ raise "Invalid interval: #{@markup}"
263
+ end
264
+ local += "/" + t2.iso8601
265
+ path += "/" + CS50::format(t2)
266
+ end
267
+
268
+ # Return element
269
+ # Even though text node will be replaced by JavaScript, ensures Kramdown can
270
+ # assign an ID if this element is used as a heading
271
+ "<span data-local='#{local}'>#{local}</span>"
272
+ end
273
+
274
+ Liquid::Template.register_tag("local", self)
275
+
276
+ end
277
+
278
+ class NextTag < Tag
279
+
280
+ def render(context)
281
+ super
282
+ button = CS50::sanitize(CS50::convert((@args[0]) ? CGI.escapeHTML(@args[0]) : "Next"))
283
+ "<button class='btn btn-dark btn-sm' data-next type='button'>#{button}</button>"
284
+ end
285
+
286
+ Liquid::Template.register_tag("next", self)
287
+
288
+ end
289
+
290
+ class SpoilerBlock < Block
291
+
292
+ # https://stackoverflow.com/q/19169849/5156190
293
+ # https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button (re phrasing, but not interactive, content)
294
+ def render(context)
295
+ html = CS50::convert(super)
296
+ summary = CS50::sanitize(CS50::convert((@args[0]) ? CGI.escapeHTML(@args[0]) : "Spoiler"))
297
+ "<details>" \
298
+ "<summary>#{summary}</summary>" \
299
+ "#{html}" \
300
+ "</details>"
301
+ end
302
+ Liquid::Template.register_tag("spoiler", self)
303
+
304
+ end
305
+
306
+ # Inspired by https://gist.github.com/niquepa/4c59b7d52a15dde2367a
307
+ class VideoTag < Tag
308
+
309
+ def render(context)
310
+ super
311
+
312
+ # Parse YouTube URL
313
+ if @args[0]
314
+
315
+ # If YouTube player
316
+ if @args[0] =~ /^https?:\/\/(?:www\.)?(?:youtube\.com\/(?:[^\/\n\s]+\/\S+\/|(?:v|e(?:mbed)?)\/|\S*?[?&]v=)|youtu\.be\/)([a-zA-Z0-9_-]{11})/
317
+
318
+ # Video's ID
319
+ v = $1
320
+
321
+ # Default components
322
+ components = {
323
+ "modestbranding" => "0",
324
+ "rel" => "0",
325
+ "showinfo" => "0"
326
+ }
327
+
328
+ # Supported components
329
+ params = CGI::parse(URI::parse(@args[0]).query || "")
330
+ ["autoplay", "controls", "end", "index", "list", "modestbranding", "mute", "playlist", "rel", "showinfo", "start", "t"].each do |param|
331
+
332
+ # If param was provided
333
+ if params.key?(param)
334
+
335
+ # Add to components, but map t= to start=
336
+ if param == "t" and !params.key?("start")
337
+ components["start"] = params["t"].first
338
+ else
339
+ components[param] = params[param].first
340
+ end
341
+ end
342
+ end
343
+
344
+ # Ensure playlist menu appears
345
+ if !params["list"].empty? or !params["playlist"].empty?
346
+ components["showinfo"] = "1"
347
+ end
348
+
349
+ # Build URL
350
+ # https://support.google.com/youtube/answer/171780?hl=en
351
+ src = URI::HTTPS.build(:host => "www.youtube.com", :path => "/embed/#{v}", :query => URI.encode_www_form(components))
352
+
353
+ # Return HTML
354
+ return "<div class='ratio ratio-16x9' data-video><iframe allow='accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture' allowfullscreen class='border' data-video src='#{src}'></iframe></div>"
355
+
356
+ # If CS50 Video Player
357
+ elsif @args[0] =~ /^https?:\/\/video\.cs50\.io\/([^?]+)/
358
+ return "<iframe allow='accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture' allowfullscreen class='border' data-video src='#{@args[0]}'></iframe>"
359
+ end
360
+ end
361
+
362
+ # Static
363
+ return "<div class='ratio ratio-16x9'><img alt='static' class='border' data-video src='https://i.imgur.com/xnZ5A2u.gif'></div>"
364
+ end
365
+
366
+ Liquid::Template.register_tag("video", self)
367
+
368
+ end
369
+
370
+ # Disable relative_url filter, since we prepend site.baseurl to all absolute paths,
371
+ # but we're not monkey-patching Jekyll::Filters::URLFilters::relative_url, since it's used by
372
+ # https://github.com/benbalter/jekyll-relative-links/blob/master/lib/jekyll-relative-links/generator.rb
373
+ module Filters
374
+ def relative_url(input)
375
+ Jekyll.logger.warn "CS50 warning: no need to use relative_url with this theme"
376
+ input
377
+ end
378
+ end
379
+ Liquid::Template.register_filter(CS50::Filters)
380
+
381
+ end
382
+
383
+ Jekyll::Hooks.register :site, :after_reset do |site|
384
+
385
+ # Override site.url so that jekyll-redirect-from doesn't prepend it
386
+ site.config["url"] = nil
387
+
388
+ # Strip trailing slashes from site.baseurl
389
+ unless site.config["baseurl"].nil?
390
+ site.config["baseurl"] = site.config["baseurl"].sub(/\/+$/, "")
391
+ end
392
+
393
+ # Disable jekyll-relative-links because it prepends site.baseurl to relative links
394
+ if site.config.key?("plugins") and site.config["plugins"].kind_of?(Array) and site.config["plugins"].include? "jekyll-relative-links"
395
+ site.config["plugins"] = site.config["plugins"] - ["jekyll-relative-links"]
396
+ Jekyll.logger.warn "CS50 warning: no need to use jekyll-relative-links with this theme"
397
+ end
398
+
399
+ # Merge in theme's configuration
400
+ site.config = CS50::DEFAULTS.dup.deep_merge!(site.config, {:merge_nil_values => true}).deep_merge!(CS50::OVERRIDES, {:merge_nil_values => true})
401
+ site.config["cs50"] = site.config["cs50"].deep_compact
402
+ end
403
+
404
+ Jekyll::Hooks.register :site, :pre_render do |site, payload|
405
+
406
+ # Expose site to Kramdown's monkey patches
407
+ $site = site
408
+
409
+ # Site's time zone
410
+ # https://stackoverflow.com/a/58867058/5156190
411
+ ENV["TZ"] = site.config["cs50"]["tz"]
412
+
413
+ # Promote site.cs50.assign.* to global variables
414
+ begin
415
+ site.config["cs50"]["assign"].each do |key, value|
416
+ payload[key] = value
417
+ end
418
+ rescue
419
+ end
420
+ end
421
+
422
+ Jekyll::Hooks.register [:site], :post_render do |site|
423
+
424
+ # Paths of pages
425
+ $paths = site.pages.map { |page| page.url }
426
+
427
+ def relative_path(from, to)
428
+
429
+ # Resolve to relative path
430
+ if !to.start_with?("/")
431
+ to = from + to
432
+ end
433
+ relative = Pathname.new(to).relative_path_from(Pathname.new(from)).to_s
434
+
435
+ # If path doesn't end with a trailing slash (before any fragment or query)
436
+ if match = relative.match(/\A([^#\?]*[^#\/\?])([#\?].*)?\z/)
437
+
438
+ # Construct absolute path
439
+ absolute = match.captures[0] + "/"
440
+ if !absolute.start_with?("/")
441
+ absolute = from + absolute
442
+ end
443
+ absolute = Pathname.new(absolute).cleanpath.to_s + "/"
444
+
445
+ # If it should have a trailing slash
446
+ if $paths.include?(absolute)
447
+
448
+ # Append trailing slash (plus any query or fragment)
449
+ relative = match.captures[0] + "/"
450
+ if !match.captures[1].nil?
451
+ relative += match.captures[1]
452
+ end
453
+ end
454
+ end
455
+
456
+ # Return path
457
+ relative
458
+ end
459
+
460
+ # For each page
461
+ site.pages.each do |page|
462
+
463
+ # If HTML
464
+ if page.output_ext == ".html"
465
+
466
+ # Parse page, including its layout
467
+ doc = Nokogiri::HTML5.parse(page.output)
468
+
469
+ # For each node in DOM
470
+ doc.traverse do |node|
471
+
472
+ # If meta
473
+ if node.name == "meta"
474
+
475
+ # Parse url
476
+ if node["content"] =~ /^(\d+;\s*url=["']?)(.+)(["']?)$/i
477
+
478
+ # If a local path
479
+ if !/^#{URI::regexp}$/.match?($2) and !$2.start_with?("//")
480
+
481
+ # Resolve as relative path
482
+ node["content"] = $1 + relative_path(page.dir, $2) + $3
483
+ end
484
+ end
485
+ end
486
+
487
+ # If one of these elements
488
+ {"a" => "href", "img" => "src", "link" => "href", "script" => "src"}.each do |name, attribute|
489
+ if node.name == name
490
+
491
+ # With a non-nil attribute
492
+ if !node[attribute].nil?
493
+
494
+ # If a local path
495
+ if node[attribute] !~ /^#{URI::regexp}$/ and !node[attribute].start_with?("//")
496
+
497
+ # Resolve as relative path
498
+ node[attribute] = relative_path(page.dir, node[attribute])
499
+ end
500
+
501
+ end
502
+ end
503
+ end
504
+ end
505
+ page.output = doc.to_html
506
+
507
+ # If CSS
508
+ elsif page.output_ext == ".css"
509
+
510
+ # Resolve absolute paths in url() to relative paths
511
+ # https://developer.mozilla.org/en-US/docs/Web/CSS/url()
512
+ page.output = page.output.gsub(/url\(\s*([^\)]*)\s*\)/) do |s|
513
+ group = "#{$1}"
514
+ if match = group.match(/\A'(\/.*)'\z/) # url('/...')
515
+ "url('" + relative_path(page.dir, match.captures[0]).to_s + "')"
516
+ elsif match = group.match(/\A"(\/.*)"\z/) # url("/...")
517
+ 'url("' + relative_path(page.dir, match.captures[0]).to_s + '")'
518
+ elsif match = group.match(/\A(\/(.*[^'"])?)\z/) # url(/...)
519
+ "url(" + relative_path(page.dir, match.captures[0]).to_s + ")"
520
+ else
521
+ s
522
+ end
523
+ end
524
+ end
525
+
526
+ # TODO: In offline mode, base64-encode images, embed CSS (in style tags) and JS (in script tags), a la
527
+ # https://github.com/jekyll/jekyll-mentions/blob/master/lib/jekyll-mentions.rb and
528
+ # https://github.com/jekyll/jemoji/blob/master/lib/jemoji.rb
529
+
530
+ end
531
+ end
532
+
533
+ # https://github.com/jekyll/jekyll-redirect-from/blob/master/lib/jekyll-redirect-from/redirect_page.rb
534
+ module JekyllRedirectFrom
535
+ class RedirectPage < Jekyll::Page
536
+ def set_paths(from, to)
537
+ @context ||= context
538
+ from = ensure_leading_slash(from)
539
+ data.merge!(
540
+ "permalink" => from,
541
+ "redirect" => {
542
+ "from" => from,
543
+ "to" => to # Omit call to absolute_url, else redirect_to misinterprets relative URLs as absolute from /
544
+ }
545
+ )
546
+ end
547
+ end
548
+ end
549
+
550
+ module Kramdown
551
+ module Parser
552
+ class GFM < Kramdown::Parser::Kramdown
553
+
554
+ def parse_autolink
555
+ super
556
+
557
+ # Get autolink
558
+ current_link = @tree.children.select{ |element| [:a].include?(element.type) }.last
559
+ unless current_link.nil?
560
+
561
+ # Hide scheme and trailing slash
562
+ current_link.children[0].value = current_link.children[0].value.gsub(/^https?:\/\/(www.)?|\/$/, "")
563
+ end
564
+ end
565
+
566
+ def parse_link
567
+ super
568
+
569
+ # Get link
570
+ current_link = @tree.children.select{ |element| [:a].include?(element.type) }.last
571
+ unless current_link.nil?
572
+
573
+ # If inline link ends with .md
574
+ if match = current_link.attr["href"].match(/\A([^\s]*)\.md(\s*.*)\z/)
575
+
576
+ # Rewrite as /, just as jekyll-relative-links does
577
+ current_link.attr["href"] = match.captures[0] + "/" + match.captures[1]
578
+ end
579
+ end
580
+ end
581
+
582
+ # Remember list markers
583
+ def parse_list
584
+ super
585
+
586
+ # Get list
587
+ current_list = @tree.children.select{ |element| [:ul].include?(element.type) }.last
588
+ unless current_list.nil?
589
+
590
+ # For each li
591
+ current_list.children.each do |li|
592
+
593
+ # Line number of li
594
+ location = li.options[:location]
595
+
596
+ # Determine marker, might be nested inside of a blockquote
597
+ # https://kramdown.gettalong.org/syntax.html#blockquotes
598
+ li.attr["data-marker"] = @source.lines[location-1].sub(/^[\s>]*/, "")[0]
599
+ end
600
+ end
601
+ true
602
+ end
603
+
604
+ end
605
+ end
606
+ end
@@ -0,0 +1 @@
1
+ # Avoids `cannot load such file -- liquid-tag-parser`