allow server to render with layout
s-ol
3 years ago
0 | import header, aside, footer, div, svg, script, g, circle, h1, span, b, a, img from require 'mmm.dom' | |
1 | import navigate_to from (require 'mmm.mmmfs.util') require 'mmm.dom' | |
2 | ||
3 | pick = (...) -> | |
4 | num = select '#', ... | |
5 | i = math.ceil math.random! * num | |
6 | select i, ... | |
7 | ||
8 | iconlink = (href, src, alt, style) -> a { | |
9 | class: 'iconlink', | |
10 | target: '_blank', | |
11 | rel: 'me', | |
12 | :href, | |
13 | img :src, :alt, :style | |
14 | } | |
15 | ||
16 | logo = svg { | |
17 | class: 'sun' | |
18 | viewBox: '-0.75 -1 1.5 2' | |
19 | xmlns: 'http://www.w3.org/2000/svg' | |
20 | baseProfile: 'full' | |
21 | version: '1.1' | |
22 | ||
23 | g { | |
24 | transform: 'translate(0 .18)' | |
25 | ||
26 | g { class: 'circle out', circle r: '.6', fill: 'none', 'stroke-width': '.12' } | |
27 | g { class: 'circle in', circle r: '.2', stroke: 'none' } | |
28 | } | |
29 | } | |
30 | ||
31 | { | |
32 | header: header { | |
33 | div { | |
34 | h1 { | |
35 | logo | |
36 | span { | |
37 | span 'mmm', class: 'bold' | |
38 | '​' | |
39 | '.s‑ol.nu' | |
40 | } | |
41 | } | |
42 | span "fun stuff with code and wires" | |
43 | -- pick 'fun', 'cool', 'weird', 'interesting', 'new' | |
44 | -- pick 'stuff', 'things', 'projects', 'experiments', 'news' | |
45 | -- "with" | |
46 | -- pick 'mostly code', 'code and wires', 'silicon', 'electronics' | |
47 | } | |
48 | aside { | |
49 | navigate_to '/about', 'about me' | |
50 | navigate_to '/games', 'games' | |
51 | navigate_to '/projects', 'other' | |
52 | a { | |
53 | href: 'mailto:s%20[removethis]%20[at]%20s-ol.nu' | |
54 | 'contact' | |
55 | script " | |
56 | var l = document.currentScript.parentElement; | |
57 | l.href = l.href.replace('%20[at]%20', '@'); | |
58 | l.href = l.href.replace('%20[removethis]', '') + '?subject=Hey there :)'; | |
59 | " | |
60 | } | |
61 | } | |
62 | } | |
63 | footer: footer { | |
64 | span { | |
65 | 'made with \xe2\x98\xbd by ' | |
66 | a 's-ol', href: 'https://twitter.com/S0lll0s' | |
67 | ", #{os.date '%Y'}" | |
68 | } | |
69 | div { | |
70 | class: 'icons', | |
71 | iconlink 'https://github.com/s-ol', 'https://cdn.jsdelivr.net/npm/simple-icons@latest/icons/github.svg', 'github' | |
72 | iconlink 'https://merveilles.town/@s_ol', 'https://cdn.jsdelivr.net/npm/simple-icons@latest/icons/mastodon.svg', 'mastodon' | |
73 | iconlink 'https://twitter.com/S0lll0s', 'https://cdn.jsdelivr.net/npm/simple-icons@latest/icons/twitter.svg', 'twitter' | |
74 | iconlink 'https://webring.xxiivv.com/#random', 'https://webring.xxiivv.com/icon.black.svg', 'webring', | |
75 | { height: '1.3em', 'margin-left': '.3em', 'margin-top': '-0.12em' } | |
76 | } | |
77 | } | |
78 | get_meta: => | |
79 | title = (@get 'title: text/plain') or @gett 'name: alpha' | |
80 | ||
81 | l = (str) -> | |
82 | str = str\gsub '[%s\\n]+$', '' | |
83 | str\gsub '\\n', ' ' | |
84 | e = (str) -> string.format '%q', l str | |
85 | ||
86 | meta = " | |
87 | <meta charset=\"UTF-8\"> | |
88 | <title>#{l title}</title> | |
89 | " | |
90 | ||
91 | if page_meta = @get '_meta: mmm/dom' | |
92 | meta ..= page_meta | |
93 | else | |
94 | meta ..= " | |
95 | <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"> | |
96 | ||
97 | <meta property=\"og:title\" content=#{e title} /> | |
98 | <meta property=\"og:type\" content=\"website\" /> | |
99 | <meta property=\"og:url\" content=\"https://mmm.s-ol.nu#{@path}/\" /> | |
100 | <meta property=\"og:site_name\" content=\"mmm\" />" | |
101 | ||
102 | if desc = @get 'description: text/plain' | |
103 | meta ..= " | |
104 | <meta property=\"og:description\" content=#{e desc} />" | |
105 | ||
106 | meta | |
107 | } |
9 | 9 | require 'mmm' |
10 | 10 | import tohtml from require 'mmm.component' |
11 | 11 | import Browser from require 'mmm.mmmfs.browser' |
12 | import get_meta, header, footer from require 'build.layout' | |
12 | import render from require 'mmm.mmmfs.layout' | |
13 | import SQLStore from require 'mmm.mmmfs.drivers.sql' | |
14 | import load_tree from require 'build.util' | |
13 | 15 | |
14 | 16 | -- usage: |
15 | -- moon render_all.moon [db.sqlite3] | |
16 | { file } = arg | |
17 | -- moon render_all.moon [db.sqlite3] [startpath] | |
18 | { file, startpath } = arg | |
17 | 19 | |
18 | 20 | export BROWSER |
19 | 21 | |
20 | render = (fileder, output) -> | |
21 | BROWSER = Browser fileder | |
22 | ||
23 | with io.open output, 'w' | |
24 | \write [[ | |
25 | <!DOCTYPE html> | |
26 | <html> | |
27 | <head> | |
28 | <link rel="stylesheet" type="text/css" href="/main.css" /> | |
29 | <!-- | |
30 | <link rel="preload" as="fetch" href="/mmm/dom/init.lua" /> | |
31 | <link rel="preload" as="fetch" href="/mmm/component/init.lua" /> | |
32 | <link rel="preload" as="fetch" href="/mmm/mmmfs/init.lua" /> | |
33 | <link rel="preload" as="fetch" href="/mmm/mmmfs/fileder.lua" /> | |
34 | <link rel="preload" as="fetch" href="/mmm/mmmfs/browser.lua" /> | |
35 | --> | |
36 | ||
37 | <link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:200,400" rel="stylesheet"> | |
38 | ]] | |
39 | \write " | |
40 | #{get_meta fileder} | |
41 | </head> | |
42 | <body> | |
43 | #{header} | |
44 | ||
45 | #{assert (tohtml BROWSER), "couldn't render BROWSER"} | |
46 | ||
47 | #{footer} | |
48 | " | |
49 | \write [[ | |
50 | <script src="/highlight.pack.js"></script> | |
51 | <script src="//cdnjs.cloudflare.com/ajax/libs/marked/0.5.1/marked.min.js"></script> | |
52 | <script src="//cdnjs.cloudflare.com/ajax/libs/svg.js/2.6.6/svg.min.js"></script> | |
53 | <script src="//platform.twitter.com/widgets.js" charset="utf-8"></script> | |
54 | <script src="/fengari-web.js"></script> | |
55 | <script type="application/lua" src="/mmm.bundle.lua"></script> | |
56 | <script type="application/lua">require 'mmm'</script> | |
57 | ]] | |
58 | \write " | |
59 | <script type=\"application/lua\"> | |
60 | on_load = on_load or {} | |
61 | table.insert(on_load, function() | |
62 | local path = #{string.format '%q', path} | |
63 | local browser = require 'mmm.mmmfs.browser' | |
64 | local root = dofile '/$bundle.lua' | |
65 | root:mount('', true) | |
66 | ||
67 | BROWSER = browser.Browser(root, path, true) | |
68 | end) | |
69 | </script> | |
70 | </body> | |
71 | </html> | |
72 | " | |
73 | \close! | |
74 | ||
75 | ||
76 | 22 | tree = load_tree SQLStore :name |
23 | tree = tree\walk startpath if startpath | |
77 | 24 | |
78 | 25 | for fileder in coroutine.wrap tree\iterate |
79 | 26 | print "rendering '#{fileder.path}'..." |
80 | 27 | os.execute "mkdir -p 'out/#{fileder.path}'" |
81 | render fileder, "out/#{fileder.path}/index.html" | |
28 | ||
29 | BROWSER = Browser fileder | |
30 | with io.open "out/#{fileder.path}/index.html", 'w' | |
31 | \write render (tohtml BROWSER), fileder | |
32 | \close! |
81 | 81 | path = req\get ':path' |
82 | 82 | |
83 | 83 | path, facet = dir_base path |
84 | print "'#{path}', '#{facet}'" | |
85 | 84 | facet = if #facet > 0 |
86 | 85 | facet = '' if facet == ':' |
87 | 86 | accept = req\get 'mmm-accept' |
94 | 93 | res = headers.new! |
95 | 94 | response_type = if status > 299 then 'text/plain' |
96 | 95 | else if facet then facet.type |
97 | else 'text/plain' | |
96 | else 'text/json' | |
98 | 97 | res\append ':status', tostring status |
99 | 98 | res\append 'content-type', response_type |
100 | 99 |
0 | import div, code, img, video, blockquote, a, span, source, iframe from require 'mmm.dom' | |
1 | import find_fileder, link_to, embed from (require 'mmm.mmmfs.util') require 'mmm.dom' | |
2 | import tohtml from require 'mmm.component' | |
3 | ||
4 | -- fix JS null values | |
5 | js_fix = if MODE == 'CLIENT' | |
6 | (arg) -> | |
7 | return if arg == js.null | |
8 | arg | |
9 | ||
10 | -- limit function to one argument | |
11 | single = (func) -> (val) -> func val | |
12 | ||
13 | -- load a chunk using a specific 'load'er | |
14 | loadwith = (_load) -> (val, fileder, key) -> | |
15 | func = assert _load val, "#{fileder}##{key}" | |
16 | func! | |
17 | ||
18 | -- list of converts | |
19 | -- converts each have | |
20 | -- * inp - input type. can capture subtypes using `(.+)` | |
21 | -- * out - output type. can substitute subtypes from inp with %1, %2 etc. | |
22 | -- * transform - function (val: inp, fileder) -> val: out | |
23 | converts = { | |
24 | { | |
25 | inp: 'fn -> (.+)', | |
26 | out: '%1', | |
27 | transform: (val, fileder) -> val fileder | |
28 | }, | |
29 | { | |
30 | inp: 'mmm/component', | |
31 | out: 'mmm/dom', | |
32 | transform: single tohtml | |
33 | }, | |
34 | { | |
35 | inp: 'mmm/dom', | |
36 | out: 'text/html', | |
37 | transform: (node) -> if MODE == 'SERVER' then node else node.outerHTML | |
38 | }, | |
39 | { | |
40 | inp: 'text/html', | |
41 | out: 'mmm/dom', | |
42 | transform: if MODE == 'SERVER' | |
43 | (html, fileder) -> | |
44 | html = html\gsub '<mmm%-link%s+(.-)>(.-)</mmm%-link>', (attrs, text) -> | |
45 | text = nil if #text == 0 | |
46 | path = '' | |
47 | while attrs and attrs != '' | |
48 | key, val, _attrs = attrs\match '^(%w+)="([^"]-)"%s*(.*)' | |
49 | if not key | |
50 | key, _attrs = attrs\match '^(%w+)%s*(.*)$' | |
51 | val = true | |
52 | ||
53 | attrs = _attrs | |
54 | ||
55 | switch key | |
56 | when 'path' then path = val | |
57 | else warn "unkown attribute '#{key}=\"#{val}\"' in <mmm-link>" | |
58 | ||
59 | link_to path, text, fileder | |
60 | ||
61 | html = html\gsub '<mmm%-embed%s+(.-)>(.-)</mmm%-embed>', (attrs, desc) -> | |
62 | path, facet = '', '' | |
63 | opts = {} | |
64 | if #desc != 0 | |
65 | opts.desc = desc | |
66 | ||
67 | while attrs and attrs != '' | |
68 | key, val, _attrs = attrs\match '^(%w+)="([^"]-)"%s*(.*)' | |
69 | if not key | |
70 | key, _attrs = attrs\match '^(%w+)%s*(.*)$' | |
71 | val = true | |
72 | ||
73 | attrs = _attrs | |
74 | ||
75 | switch key | |
76 | when 'path' then path = val | |
77 | when 'facet' then facet = val | |
78 | when 'nolink' then opts.nolink = true | |
79 | when 'inline' then opts.inline = true | |
80 | else warn "unkown attribute '#{key}=\"#{val}\"' in <mmm-embed>" | |
81 | ||
82 | embed path, facet, fileder, opts | |
83 | ||
84 | html | |
85 | else | |
86 | (html, fileder) -> | |
87 | parent = with document\createElement 'div' | |
88 | .innerHTML = html | |
89 | ||
90 | -- copy to iterate safely, HTMLCollections update when nodes are GC'ed | |
91 | embeds = \getElementsByTagName 'mmm-embed' | |
92 | embeds = [embeds[i] for i=0, embeds.length - 1] | |
93 | for element in *embeds | |
94 | path = js_fix element\getAttribute 'path' | |
95 | facet = js_fix element\getAttribute 'facet' | |
96 | nolink = js_fix element\getAttribute 'nolink' | |
97 | inline = js_fix element\getAttribute 'inline' | |
98 | desc = js_fix element.innerText | |
99 | ||
100 | element\replaceWith embed path or '', facet or '', fileder, { :nolink, :inline, :desc } | |
101 | ||
102 | embeds = \getElementsByTagName 'mmm-link' | |
103 | embeds = [embeds[i] for i=0, embeds.length - 1] | |
104 | for element in *embeds | |
105 | text = js_fix element.innerText | |
106 | path = js_fix element\getAttribute 'path' | |
107 | ||
108 | element\replaceWith link_to path or '', text, fileder | |
109 | ||
110 | assert 1 == parent.childElementCount, "text/html with more than one child!" | |
111 | parent.firstElementChild | |
112 | }, | |
113 | { | |
114 | inp: 'text/lua -> (.+)', | |
115 | out: '%1', | |
116 | transform: loadwith load or loadstring | |
117 | }, | |
118 | { | |
119 | inp: 'mmm/tpl -> (.+)', | |
120 | out: '%1', | |
121 | transform: (source, fileder) -> | |
122 | source\gsub '{{(.-)}}', (expr) -> | |
123 | path, facet = expr\match '^([%w%-_%./]*)%+(.*)' | |
124 | assert path, "couldn't match TPL expression '#{expr}'" | |
125 | ||
126 | (find_fileder path, fileder)\gett facet | |
127 | }, | |
128 | { | |
129 | inp: 'time/iso8601-date', | |
130 | out: 'time/unix', | |
131 | transform: (val) -> | |
132 | year, _, month, day = val\match '^%s*(%d%d%d%d)(%-?)([01]%d)%2([0-3]%d)%s*$' | |
133 | assert year, "failed to parse ISO 8601 date: '#{val}'" | |
134 | os.time :year, :month, :day | |
135 | }, | |
136 | { | |
137 | inp: 'URL -> twitter/tweet', | |
138 | out: 'mmm/dom', | |
139 | transform: (href) -> | |
140 | id = assert (href\match 'twitter.com/[^/]-/status/(%d*)'), "couldn't parse twitter/tweet URL: '#{href}'" | |
141 | if MODE == 'CLIENT' | |
142 | with parent = div! | |
143 | window.twttr.widgets\createTweet id, parent | |
144 | else | |
145 | div blockquote { | |
146 | class: 'twitter-tweet' | |
147 | 'data-lang': 'en' | |
148 | a '(linked tweet)', :href | |
149 | } | |
150 | }, | |
151 | { | |
152 | inp: 'URL -> youtube/video', | |
153 | out: 'mmm/dom', | |
154 | transform: (link) -> | |
155 | id = link\match 'youtu%.be/([^/]+)' | |
156 | id or= link\match 'youtube.com/watch.*[?&]v=([^&]+)' | |
157 | id or= link\match 'youtube.com/[ev]/([^/]+)' | |
158 | id or= link\match 'youtube.com/embed/([^/]+)' | |
159 | ||
160 | assert id, "couldn't parse youtube URL: '#{link}'" | |
161 | ||
162 | iframe { | |
163 | width: 560 | |
164 | height: 315 | |
165 | frameborder: 0 | |
166 | allowfullscreen: true | |
167 | frameBorder: 0 | |
168 | src: "//www.youtube.com/embed/#{id}" | |
169 | } | |
170 | }, | |
171 | { | |
172 | inp: 'URL -> image/.+', | |
173 | out: 'mmm/dom', | |
174 | transform: (src, fileder) -> img :src | |
175 | }, | |
176 | { | |
177 | inp: 'URL -> video/.+', | |
178 | out: 'mmm/dom', | |
179 | transform: (src) -> | |
180 | -- @TODO: add parsed MIME type | |
181 | video (source :src), controls: true, loop: true | |
182 | }, | |
183 | { | |
184 | inp: 'text/plain', | |
185 | out: 'mmm/dom', | |
186 | transform: (val) -> span val | |
187 | }, | |
188 | { | |
189 | inp: 'alpha', | |
190 | out: 'mmm/dom', | |
191 | transform: single code | |
192 | }, | |
193 | { | |
194 | inp: 'URL -> .*', | |
195 | out: 'mmm/dom', | |
196 | transform: single code | |
197 | }, | |
198 | } | |
199 | ||
200 | if MODE == 'SERVER' | |
201 | ok, moon = pcall require, 'moonscript.base' | |
202 | if ok | |
203 | _load = moon.load or moon.loadstring | |
204 | table.insert converts, { | |
205 | inp: 'text/moonscript -> (.+)', | |
206 | out: '%1', | |
207 | transform: loadwith moon.load or moon.loadstring | |
208 | } | |
209 | ||
210 | table.insert converts, { | |
211 | inp: 'text/moonscript -> (.+)', | |
212 | out: 'text/lua -> %1', | |
213 | transform: single moon.to_lua | |
214 | } | |
215 | else | |
216 | table.insert converts, { | |
217 | inp: 'text/javascript -> (.+)', | |
218 | out: '%1', | |
219 | transform: (source) -> | |
220 | f = js.new window.Function, source | |
221 | f! | |
222 | } | |
223 | ||
224 | do | |
225 | local markdown | |
226 | if MODE == 'SERVER' | |
227 | success, discount = pcall require, 'discount' | |
228 | if not success | |
229 | warn "NO MARKDOWN SUPPORT!", discount | |
230 | ||
231 | markdown = success and (md) -> | |
232 | res = assert discount.compile md, 'githubtags' | |
233 | res.body | |
234 | else | |
235 | markdown = window and window.marked and window\marked | |
236 | ||
237 | if markdown | |
238 | table.insert converts, { | |
239 | inp: 'text/markdown', | |
240 | out: 'text/html', | |
241 | transform: (md) -> "<div class=\"markdown\">#{markdown md}</div>" | |
242 | } | |
243 | ||
244 | table.insert converts, { | |
245 | inp: 'text/markdown%+span', | |
246 | out: 'mmm/dom', | |
247 | transform: if MODE == 'SERVER' | |
248 | (source) -> | |
249 | html = markdown source | |
250 | html = html\gsub '^<p', '<span' | |
251 | html\gsub '/p>$', '/span>' | |
252 | else | |
253 | (source) -> | |
254 | html = markdown source | |
255 | html = html\gsub '^%s*<p>%s*', '' | |
256 | html = html\gsub '%s*</p>%s*$', '' | |
257 | with document\createElement 'span' | |
258 | .innerHTML = html | |
259 | } | |
0 | require = relative ..., 1 | |
1 | converts = require '.converts' | |
260 | 2 | |
261 | 3 | count = (base, pattern='->') -> select 2, base\gsub pattern, '' |
262 | 4 | escape_pattern = (inp) -> "^#{inp\gsub '([-/])', '%%%1'}$" |
0 | require = relative ..., 1 | |
1 | import div, code, img, video, blockquote, a, span, source, iframe from require 'mmm.dom' | |
2 | import find_fileder, link_to, embed from (require 'mmm.mmmfs.util') require 'mmm.dom' | |
3 | import render from require '.layout' | |
4 | import tohtml from require 'mmm.component' | |
5 | ||
6 | -- fix JS null values | |
7 | js_fix = if MODE == 'CLIENT' | |
8 | (arg) -> | |
9 | return if arg == js.null | |
10 | arg | |
11 | ||
12 | -- limit function to one argument | |
13 | single = (func) -> (val) -> func val | |
14 | ||
15 | -- load a chunk using a specific 'load'er | |
16 | loadwith = (_load) -> (val, fileder, key) -> | |
17 | func = assert _load val, "#{fileder}##{key}" | |
18 | func! | |
19 | ||
20 | -- list of converts | |
21 | -- converts each have | |
22 | -- * inp - input type. can capture subtypes using `(.+)` | |
23 | -- * out - output type. can substitute subtypes from inp with %1, %2 etc. | |
24 | -- * transform - function (val: inp, fileder) -> val: out | |
25 | converts = { | |
26 | { | |
27 | inp: 'fn -> (.+)', | |
28 | out: '%1', | |
29 | transform: (val, fileder) -> val fileder | |
30 | }, | |
31 | { | |
32 | inp: 'mmm/component', | |
33 | out: 'mmm/dom', | |
34 | transform: single tohtml | |
35 | }, | |
36 | { | |
37 | inp: 'mmm/dom', | |
38 | out: 'text/html+frag', | |
39 | transform: (node) -> if MODE == 'SERVER' then node else node.outerHTML | |
40 | }, | |
41 | { | |
42 | inp: 'text/html%+frag', | |
43 | out: 'text/html', | |
44 | transform: (html, fileder) -> render html, fileder | |
45 | }, | |
46 | { | |
47 | inp: 'text/html%+frag', | |
48 | out: 'mmm/dom', | |
49 | transform: if MODE == 'SERVER' | |
50 | (html, fileder) -> | |
51 | html = html\gsub '<mmm%-link%s+(.-)>(.-)</mmm%-link>', (attrs, text) -> | |
52 | text = nil if #text == 0 | |
53 | path = '' | |
54 | while attrs and attrs != '' | |
55 | key, val, _attrs = attrs\match '^(%w+)="([^"]-)"%s*(.*)' | |
56 | if not key | |
57 | key, _attrs = attrs\match '^(%w+)%s*(.*)$' | |
58 | val = true | |
59 | ||
60 | attrs = _attrs | |
61 | ||
62 | switch key | |
63 | when 'path' then path = val | |
64 | else warn "unkown attribute '#{key}=\"#{val}\"' in <mmm-link>" | |
65 | ||
66 | link_to path, text, fileder | |
67 | ||
68 | html = html\gsub '<mmm%-embed%s+(.-)>(.-)</mmm%-embed>', (attrs, desc) -> | |
69 | path, facet = '', '' | |
70 | opts = {} | |
71 | if #desc != 0 | |
72 | opts.desc = desc | |
73 | ||
74 | while attrs and attrs != '' | |
75 | key, val, _attrs = attrs\match '^(%w+)="([^"]-)"%s*(.*)' | |
76 | if not key | |
77 | key, _attrs = attrs\match '^(%w+)%s*(.*)$' | |
78 | val = true | |
79 | ||
80 | attrs = _attrs | |
81 | ||
82 | switch key | |
83 | when 'path' then path = val | |
84 | when 'facet' then facet = val | |
85 | when 'nolink' then opts.nolink = true | |
86 | when 'inline' then opts.inline = true | |
87 | else warn "unkown attribute '#{key}=\"#{val}\"' in <mmm-embed>" | |
88 | ||
89 | embed path, facet, fileder, opts | |
90 | ||
91 | html | |
92 | else | |
93 | (html, fileder) -> | |
94 | parent = with document\createElement 'div' | |
95 | .innerHTML = html | |
96 | ||
97 | -- copy to iterate safely, HTMLCollections update when nodes are GC'ed | |
98 | embeds = \getElementsByTagName 'mmm-embed' | |
99 | embeds = [embeds[i] for i=0, embeds.length - 1] | |
100 | for element in *embeds | |
101 | path = js_fix element\getAttribute 'path' | |
102 | facet = js_fix element\getAttribute 'facet' | |
103 | nolink = js_fix element\getAttribute 'nolink' | |
104 | inline = js_fix element\getAttribute 'inline' | |
105 | desc = js_fix element.innerText | |
106 | ||
107 | element\replaceWith embed path or '', facet or '', fileder, { :nolink, :inline, :desc } | |
108 | ||
109 | embeds = \getElementsByTagName 'mmm-link' | |
110 | embeds = [embeds[i] for i=0, embeds.length - 1] | |
111 | for element in *embeds | |
112 | text = js_fix element.innerText | |
113 | path = js_fix element\getAttribute 'path' | |
114 | ||
115 | element\replaceWith link_to path or '', text, fileder | |
116 | ||
117 | assert 1 == parent.childElementCount, "text/html with more than one child!" | |
118 | parent.firstElementChild | |
119 | }, | |
120 | { | |
121 | inp: 'text/lua -> (.+)', | |
122 | out: '%1', | |
123 | transform: loadwith load or loadstring | |
124 | }, | |
125 | { | |
126 | inp: 'mmm/tpl -> (.+)', | |
127 | out: '%1', | |
128 | transform: (source, fileder) -> | |
129 | source\gsub '{{(.-)}}', (expr) -> | |
130 | path, facet = expr\match '^([%w%-_%./]*)%+(.*)' | |
131 | assert path, "couldn't match TPL expression '#{expr}'" | |
132 | ||
133 | (find_fileder path, fileder)\gett facet | |
134 | }, | |
135 | { | |
136 | inp: 'time/iso8601-date', | |
137 | out: 'time/unix', | |
138 | transform: (val) -> | |
139 | year, _, month, day = val\match '^%s*(%d%d%d%d)(%-?)([01]%d)%2([0-3]%d)%s*$' | |
140 | assert year, "failed to parse ISO 8601 date: '#{val}'" | |
141 | os.time :year, :month, :day | |
142 | }, | |
143 | { | |
144 | inp: 'URL -> twitter/tweet', | |
145 | out: 'mmm/dom', | |
146 | transform: (href) -> | |
147 | id = assert (href\match 'twitter.com/[^/]-/status/(%d*)'), "couldn't parse twitter/tweet URL: '#{href}'" | |
148 | if MODE == 'CLIENT' | |
149 | with parent = div! | |
150 | window.twttr.widgets\createTweet id, parent | |
151 | else | |
152 | div blockquote { | |
153 | class: 'twitter-tweet' | |
154 | 'data-lang': 'en' | |
155 | a '(linked tweet)', :href | |
156 | } | |
157 | }, | |
158 | { | |
159 | inp: 'URL -> youtube/video', | |
160 | out: 'mmm/dom', | |
161 | transform: (link) -> | |
162 | id = link\match 'youtu%.be/([^/]+)' | |
163 | id or= link\match 'youtube.com/watch.*[?&]v=([^&]+)' | |
164 | id or= link\match 'youtube.com/[ev]/([^/]+)' | |
165 | id or= link\match 'youtube.com/embed/([^/]+)' | |
166 | ||
167 | assert id, "couldn't parse youtube URL: '#{link}'" | |
168 | ||
169 | iframe { | |
170 | width: 560 | |
171 | height: 315 | |
172 | frameborder: 0 | |
173 | allowfullscreen: true | |
174 | frameBorder: 0 | |
175 | src: "//www.youtube.com/embed/#{id}" | |
176 | } | |
177 | }, | |
178 | { | |
179 | inp: 'URL -> image/.+', | |
180 | out: 'mmm/dom', | |
181 | transform: (src, fileder) -> img :src | |
182 | }, | |
183 | { | |
184 | inp: 'URL -> video/.+', | |
185 | out: 'mmm/dom', | |
186 | transform: (src) -> | |
187 | -- @TODO: add parsed MIME type | |
188 | video (source :src), controls: true, loop: true | |
189 | }, | |
190 | { | |
191 | inp: 'text/plain', | |
192 | out: 'mmm/dom', | |
193 | transform: (val) -> span val | |
194 | }, | |
195 | { | |
196 | inp: 'alpha', | |
197 | out: 'mmm/dom', | |
198 | transform: single code | |
199 | }, | |
200 | { | |
201 | inp: 'URL -> .*', | |
202 | out: 'mmm/dom', | |
203 | transform: single code | |
204 | }, | |
205 | } | |
206 | ||
207 | if MODE == 'SERVER' | |
208 | ok, moon = pcall require, 'moonscript.base' | |
209 | if ok | |
210 | _load = moon.load or moon.loadstring | |
211 | table.insert converts, { | |
212 | inp: 'text/moonscript -> (.+)', | |
213 | out: '%1', | |
214 | transform: loadwith moon.load or moon.loadstring | |
215 | } | |
216 | ||
217 | table.insert converts, { | |
218 | inp: 'text/moonscript -> (.+)', | |
219 | out: 'text/lua -> %1', | |
220 | transform: single moon.to_lua | |
221 | } | |
222 | else | |
223 | table.insert converts, { | |
224 | inp: 'text/javascript -> (.+)', | |
225 | out: '%1', | |
226 | transform: (source) -> | |
227 | f = js.new window.Function, source | |
228 | f! | |
229 | } | |
230 | ||
231 | do | |
232 | local markdown | |
233 | if MODE == 'SERVER' | |
234 | success, discount = pcall require, 'discount' | |
235 | if not success | |
236 | warn "NO MARKDOWN SUPPORT!", discount | |
237 | ||
238 | markdown = success and (md) -> | |
239 | res = assert discount.compile md, 'githubtags' | |
240 | res.body | |
241 | else | |
242 | markdown = window and window.marked and window\marked | |
243 | ||
244 | if markdown | |
245 | table.insert converts, { | |
246 | inp: 'text/markdown', | |
247 | out: 'text/html+frag', | |
248 | transform: (md) -> "<div class=\"markdown\">#{markdown md}</div>" | |
249 | } | |
250 | ||
251 | table.insert converts, { | |
252 | inp: 'text/markdown%+span', | |
253 | out: 'mmm/dom', | |
254 | transform: if MODE == 'SERVER' | |
255 | (source) -> | |
256 | html = markdown source | |
257 | html = html\gsub '^<p', '<span' | |
258 | html\gsub '/p>$', '/span>' | |
259 | else | |
260 | (source) -> | |
261 | html = markdown source | |
262 | html = html\gsub '^%s*<p>%s*', '' | |
263 | html = html\gsub '%s*</p>%s*$', '' | |
264 | with document\createElement 'span' | |
265 | .innerHTML = html | |
266 | } | |
267 | ||
268 | converts |
0 | require = relative ..., 1 | |
1 | import header, aside, footer, div, svg, script, g, circle, h1, span, b, a, img from require 'mmm.dom' | |
2 | import navigate_to from (require 'mmm.mmmfs.util') require 'mmm.dom' | |
3 | ||
4 | pick = (...) -> | |
5 | num = select '#', ... | |
6 | i = math.ceil math.random! * num | |
7 | select i, ... | |
8 | ||
9 | iconlink = (href, src, alt, style) -> a { | |
10 | class: 'iconlink', | |
11 | target: '_blank', | |
12 | rel: 'me', | |
13 | :href, | |
14 | img :src, :alt, :style | |
15 | } | |
16 | ||
17 | logo = svg { | |
18 | class: 'sun' | |
19 | viewBox: '-0.75 -1 1.5 2' | |
20 | xmlns: 'http://www.w3.org/2000/svg' | |
21 | baseProfile: 'full' | |
22 | version: '1.1' | |
23 | ||
24 | g { | |
25 | transform: 'translate(0 .18)' | |
26 | ||
27 | g { class: 'circle out', circle r: '.6', fill: 'none', 'stroke-width': '.12' } | |
28 | g { class: 'circle in', circle r: '.2', stroke: 'none' } | |
29 | } | |
30 | } | |
31 | ||
32 | header = header { | |
33 | div { | |
34 | h1 { | |
35 | logo | |
36 | span { | |
37 | span 'mmm', class: 'bold' | |
38 | '​' | |
39 | '.s‑ol.nu' | |
40 | } | |
41 | } | |
42 | span "fun stuff with code and wires" | |
43 | -- pick 'fun', 'cool', 'weird', 'interesting', 'new' | |
44 | -- pick 'stuff', 'things', 'projects', 'experiments', 'news' | |
45 | -- "with" | |
46 | -- pick 'mostly code', 'code and wires', 'silicon', 'electronics' | |
47 | } | |
48 | aside { | |
49 | navigate_to '/about', 'about me' | |
50 | navigate_to '/games', 'games' | |
51 | navigate_to '/projects', 'other' | |
52 | a { | |
53 | href: 'mailto:s%20[removethis]%20[at]%20s-ol.nu' | |
54 | 'contact' | |
55 | script " | |
56 | var l = document.currentScript.parentElement; | |
57 | l.href = l.href.replace('%20[at]%20', '@'); | |
58 | l.href = l.href.replace('%20[removethis]', '') + '?subject=Hey there :)'; | |
59 | " | |
60 | } | |
61 | } | |
62 | } | |
63 | ||
64 | footer = footer { | |
65 | span { | |
66 | 'made with \xe2\x98\xbd by ' | |
67 | a 's-ol', href: 'https://twitter.com/S0lll0s' | |
68 | ", #{os.date '%Y'}" | |
69 | } | |
70 | div { | |
71 | class: 'icons', | |
72 | iconlink 'https://github.com/s-ol', 'https://cdn.jsdelivr.net/npm/simple-icons@latest/icons/github.svg', 'github' | |
73 | iconlink 'https://merveilles.town/@s_ol', 'https://cdn.jsdelivr.net/npm/simple-icons@latest/icons/mastodon.svg', 'mastodon' | |
74 | iconlink 'https://twitter.com/S0lll0s', 'https://cdn.jsdelivr.net/npm/simple-icons@latest/icons/twitter.svg', 'twitter' | |
75 | iconlink 'https://webring.xxiivv.com/#random', 'https://webring.xxiivv.com/icon.black.svg', 'webring', | |
76 | { height: '1.3em', 'margin-left': '.3em', 'margin-top': '-0.12em' } | |
77 | } | |
78 | } | |
79 | ||
80 | get_meta = => | |
81 | title = (@get 'title: text/plain') or @gett 'name: alpha' | |
82 | ||
83 | l = (str) -> | |
84 | str = str\gsub '[%s\\n]+$', '' | |
85 | str\gsub '\\n', ' ' | |
86 | e = (str) -> string.format '%q', l str | |
87 | ||
88 | meta = " | |
89 | <meta charset=\"UTF-8\"> | |
90 | <title>#{l title}</title> | |
91 | " | |
92 | ||
93 | if page_meta = @get '_meta: mmm/dom' | |
94 | meta ..= page_meta | |
95 | else | |
96 | meta ..= " | |
97 | <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"> | |
98 | ||
99 | <meta property=\"og:title\" content=#{e title} /> | |
100 | <meta property=\"og:type\" content=\"website\" /> | |
101 | <meta property=\"og:url\" content=\"https://mmm.s-ol.nu#{@path}/\" /> | |
102 | <meta property=\"og:site_name\" content=\"mmm\" />" | |
103 | ||
104 | if desc = @get 'description: text/plain' | |
105 | meta ..= " | |
106 | <meta property=\"og:description\" content=#{e desc} />" | |
107 | ||
108 | meta | |
109 | ||
110 | render = (content, fileder) -> | |
111 | buf = [[ | |
112 | <!DOCTYPE html> | |
113 | <html> | |
114 | <head> | |
115 | <link rel="stylesheet" type="text/css" href="/main.css" /> | |
116 | <!-- | |
117 | <link rel="preload" as="fetch" href="/mmm/dom/init.lua" /> | |
118 | <link rel="preload" as="fetch" href="/mmm/component/init.lua" /> | |
119 | <link rel="preload" as="fetch" href="/mmm/mmmfs/init.lua" /> | |
120 | <link rel="preload" as="fetch" href="/mmm/mmmfs/fileder.lua" /> | |
121 | <link rel="preload" as="fetch" href="/mmm/mmmfs/browser.lua" /> | |
122 | --> | |
123 | ||
124 | <link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:200,400" rel="stylesheet"> | |
125 | ]] | |
126 | buf ..= " | |
127 | #{get_meta fileder} | |
128 | </head> | |
129 | <body> | |
130 | #{header} | |
131 | ||
132 | #{content} | |
133 | ||
134 | #{footer} | |
135 | " | |
136 | buf ..= [[ | |
137 | <script src="/highlight.pack.js"></script> | |
138 | <script src="//cdnjs.cloudflare.com/ajax/libs/marked/0.5.1/marked.min.js"></script> | |
139 | <script src="//cdnjs.cloudflare.com/ajax/libs/svg.js/2.6.6/svg.min.js"></script> | |
140 | <script src="//platform.twitter.com/widgets.js" charset="utf-8"></script> | |
141 | <script src="/fengari-web.js"></script> | |
142 | <script type="application/lua" src="/mmm.bundle.lua"></script> | |
143 | <script type="application/lua">require 'mmm'</script> | |
144 | ]] | |
145 | buf ..= " | |
146 | <script type=\"application/lua\"> | |
147 | on_load = on_load or {} | |
148 | table.insert(on_load, function() | |
149 | local path = #{string.format '%q', path} | |
150 | local browser = require 'mmm.mmmfs.browser' | |
151 | local root = dofile '/$bundle.lua' | |
152 | root:mount('', true) | |
153 | ||
154 | BROWSER = browser.Browser(root, path, true) | |
155 | end) | |
156 | </script> | |
157 | </body> | |
158 | </html> | |
159 | " | |
160 | ||
161 | buf | |
162 | ||
163 | { | |
164 | :render | |
165 | } |