new type/pathfinding algorithm
s-ol
3 years ago
0 | 0 | require = relative ..., 1 |
1 | 1 | import Key from require '.fileder' |
2 | import converts, get_conversions, apply_conversions from require '.conversion' | |
2 | import get_conversions, apply_conversions from require '.conversion' | |
3 | 3 | import ReactiveVar, get_or_create, text, elements, tohtml from require 'mmm.component' |
4 | 4 | import pre, div, nav, span, button, a, code, select, option from elements |
5 | 5 | import languages from require 'mmm.highlighting' |
6 | converts = require '.converts' | |
6 | 7 | |
7 | 8 | keep = (var) -> |
8 | 9 | last = var\get! |
14 | 15 | { |
15 | 16 | inp: "text/#{lang}.*", |
16 | 17 | out: 'mmm/dom', |
18 | cost: 0 | |
17 | 19 | transform: (val) => languages[lang] val |
18 | 20 | } |
19 | 21 | |
27 | 29 | { |
28 | 30 | inp: 'URL.*' |
29 | 31 | out: 'mmm/dom' |
32 | cost: 0 | |
30 | 33 | transform: (href) => span a (code href), :href |
31 | 34 | } |
32 | 35 | } |
0 | 0 | require = relative ..., 1 |
1 | converts = require '.converts' | |
1 | base_converts = require '.converts' | |
2 | import Queue from require '.queue' | |
2 | 3 | |
3 | 4 | count = (base, pattern='->') -> select 2, base\gsub pattern, '' |
4 | 5 | escape_pattern = (inp) -> "^#{inp\gsub '([^%w])', '%%%1'}$" |
5 | 6 | escape_inp = (inp) -> "^#{inp\gsub '([-/])', '%%%1'}$" |
7 | ||
8 | local print_conversions | |
6 | 9 | |
7 | 10 | -- attempt to find a conversion path from 'have' to 'want' |
8 | 11 | -- * have - start type string or list of type strings |
9 | 12 | -- * want - stop type pattern |
10 | 13 | -- * limit - limit conversion amount |
11 | 14 | -- returns a list of conversion steps |
12 | get_conversions = (want, have, _converts=converts, limit=5) -> | |
15 | get_conversions = (want, have, converts=base_converts, limit=5) -> | |
13 | 16 | assert have, 'need starting type(s)' |
14 | 17 | |
15 | 18 | if 'string' == type have |
18 | 21 | assert #have > 0, 'need starting type(s) (list was empty)' |
19 | 22 | |
20 | 23 | want = escape_pattern want |
21 | iterations = limit + math.max table.unpack [count type for type in *have] | |
22 | have = [{ :start, rest: start, conversions: {} } for start in *have] | |
24 | limit = limit + 3 * math.max table.unpack [count type for type in *have] | |
23 | 25 | |
24 | for i=1, iterations | |
25 | next_have, c = {}, 1 | |
26 | for { :start, :rest, :conversions } in *have | |
27 | if rest\match want | |
28 | return conversions, start | |
26 | had = {} | |
27 | queue = Queue! | |
28 | for start in *have | |
29 | return {}, start if want\match start | |
30 | queue\add { :start, rest: start, conversions: {} }, 0, start | |
31 | ||
32 | best = Queue! | |
33 | ||
34 | while true | |
35 | entry, cost = queue\pop! | |
36 | if not entry or cost > limit | |
37 | break | |
38 | ||
39 | { :start, :rest, :conversions } = entry | |
40 | had[rest] = true | |
41 | for convert in *converts | |
42 | inp = escape_inp convert.inp | |
43 | continue unless rest\match inp | |
44 | result = rest\gsub inp, convert.out | |
45 | continue unless result | |
46 | continue if had[result] | |
47 | ||
48 | next_entry = { | |
49 | :start | |
50 | rest: result | |
51 | cost: cost + convert.cost | |
52 | conversions: { { :convert, from: rest, to: result }, table.unpack conversions } | |
53 | } | |
54 | ||
55 | if result\match want | |
56 | best\add next_entry, next_entry.cost | |
29 | 57 | else |
30 | for convert in *_converts | |
31 | inp = escape_inp convert.inp | |
32 | continue unless rest\match inp | |
33 | result = rest\gsub inp, convert.out | |
34 | if result | |
35 | next_have[c] = { | |
36 | :start, | |
37 | rest: result, | |
38 | conversions: { { :convert, from: rest, to: result }, table.unpack conversions } | |
39 | } | |
40 | c += 1 | |
58 | queue\add next_entry, next_entry.cost, result | |
41 | 59 | |
42 | have = next_have | |
43 | return unless #have > 0 | |
60 | ||
61 | if solution = best\pop! | |
62 | -- print "BEST: (#{solution.cost})" | |
63 | -- print_conversions solution.conversions | |
64 | solution.conversions, solution.start if solution | |
65 | ||
66 | -- stringify conversions for debugging | |
67 | -- * conversions - conversions from get_conversions | |
68 | print_conversions = (conversions) -> | |
69 | print "converting:" | |
70 | for i=#conversions,1,-1 | |
71 | step = conversions[i] | |
72 | print "- #{step.from} -> #{step.to} (#{step.convert.cost})" | |
44 | 73 | |
45 | 74 | -- apply transforms for conversion path sequentially |
46 | 75 | -- * conversions - conversions from get_conversions |
72 | 101 | apply_conversions conversions, value, ... |
73 | 102 | |
74 | 103 | { |
75 | :converts | |
76 | 104 | :get_conversions |
77 | 105 | :apply_conversions |
78 | 106 | :convert |
29 | 29 | { |
30 | 30 | inp: "text/#{lang}", |
31 | 31 | out: 'mmm/dom', |
32 | cost: 5 | |
32 | 33 | transform: (val) => pre languages[lang] val |
33 | 34 | } |
34 | 35 | |
41 | 42 | { |
42 | 43 | inp: 'fn -> (.+)', |
43 | 44 | out: '%1', |
45 | cost: 1 | |
44 | 46 | transform: (val, fileder) => val fileder |
45 | 47 | } |
46 | 48 | { |
47 | 49 | inp: 'mmm/component', |
48 | 50 | out: 'mmm/dom', |
51 | const: 3 | |
49 | 52 | transform: single tohtml |
50 | 53 | } |
51 | 54 | { |
52 | 55 | inp: 'mmm/dom', |
53 | 56 | out: 'text/html+frag', |
57 | cost: 3 | |
54 | 58 | transform: (node) => if MODE == 'SERVER' then node else node.outerHTML |
55 | 59 | } |
56 | 60 | { |
58 | 62 | -- @TODO: this doesn't feel right... maybe mmm/dom has to go? |
59 | 63 | inp: 'mmm/dom', |
60 | 64 | out: 'text/html', |
65 | cost: 3 | |
61 | 66 | transform: (html, fileder) => render html, fileder |
62 | 67 | } |
63 | 68 | { |
64 | 69 | inp: 'text/html%+frag', |
65 | 70 | out: 'mmm/dom', |
71 | cost: 1 | |
66 | 72 | transform: if MODE == 'SERVER' |
67 | 73 | (html, fileder) => |
68 | 74 | html = html\gsub '<mmm%-link%s+(.-)>(.-)</mmm%-link>', (attrs, text) -> |
137 | 143 | { |
138 | 144 | inp: 'text/lua -> (.+)', |
139 | 145 | out: '%1', |
146 | cost: 0.5 | |
140 | 147 | transform: loadwith load or loadstring |
141 | 148 | } |
142 | 149 | { |
143 | 150 | inp: 'mmm/tpl -> (.+)', |
144 | 151 | out: '%1', |
152 | cost: 1 | |
145 | 153 | transform: (source, fileder) => |
146 | 154 | source\gsub '{{(.-)}}', (expr) -> |
147 | 155 | path, facet = expr\match '^([%w%-_%./]*)%+(.*)' |
152 | 160 | { |
153 | 161 | inp: 'time/iso8601-date', |
154 | 162 | out: 'time/unix', |
163 | cost: 0.5 | |
155 | 164 | transform: (val) => |
156 | 165 | year, _, month, day = val\match '^%s*(%d%d%d%d)(%-?)([01]%d)%2([0-3]%d)%s*$' |
157 | 166 | assert year, "failed to parse ISO 8601 date: '#{val}'" |
160 | 169 | { |
161 | 170 | inp: 'URL -> twitter/tweet', |
162 | 171 | out: 'mmm/dom', |
172 | cost: 1 | |
163 | 173 | transform: (href) => |
164 | 174 | id = assert (href\match 'twitter.com/[^/]-/status/(%d*)'), "couldn't parse twitter/tweet URL: '#{href}'" |
165 | 175 | if MODE == 'CLIENT' |
175 | 185 | { |
176 | 186 | inp: 'URL -> youtube/video', |
177 | 187 | out: 'mmm/dom', |
188 | cost: 1 | |
178 | 189 | transform: (link) => |
179 | 190 | id = link\match 'youtu%.be/([^/]+)' |
180 | 191 | id or= link\match 'youtube.com/watch.*[?&]v=([^&]+)' |
195 | 206 | { |
196 | 207 | inp: 'URL -> image/.+', |
197 | 208 | out: 'mmm/dom', |
209 | cost: 1 | |
198 | 210 | transform: (src, fileder) => img :src |
199 | 211 | } |
200 | 212 | { |
201 | 213 | inp: 'URL -> video/.+', |
202 | 214 | out: 'mmm/dom', |
215 | cost: 1 | |
203 | 216 | transform: (src) => |
204 | 217 | -- @TODO: add parsed MIME type |
205 | 218 | video (source :src), controls: true, loop: true |
207 | 220 | { |
208 | 221 | inp: 'text/plain', |
209 | 222 | out: 'mmm/dom', |
223 | cost: 2 | |
210 | 224 | transform: (val) => span val |
211 | 225 | } |
212 | 226 | { |
213 | 227 | inp: 'alpha', |
214 | 228 | out: 'mmm/dom', |
229 | cost: 2 | |
215 | 230 | transform: single code |
216 | 231 | } |
217 | 232 | -- this one needs a higher cost |
223 | 238 | { |
224 | 239 | inp: '(.+)', |
225 | 240 | out: 'URL -> %1', |
241 | cost: 10 | |
226 | 242 | transform: (_, fileder, key) => "#{fileder.path}/#{key.name}:#{@from}" |
227 | 243 | } |
228 | 244 | { |
229 | 245 | inp: 'table', |
230 | 246 | out: 'text/json', |
247 | cost: 2 | |
231 | 248 | transform: do |
232 | 249 | tojson = (obj) -> |
233 | 250 | switch type obj |
252 | 269 | { |
253 | 270 | inp: 'table', |
254 | 271 | out: 'mmm/dom', |
272 | cost: 5 | |
255 | 273 | transform: do |
256 | 274 | deep_tostring = (tbl, space='') -> |
257 | 275 | buf = space .. tostring tbl |
280 | 298 | table.insert converts, { |
281 | 299 | inp: 'text/moonscript -> (.+)', |
282 | 300 | out: '%1', |
301 | cost: 1 | |
283 | 302 | transform: loadwith moon.load or moon.loadstring |
284 | 303 | } |
285 | 304 | |
286 | 305 | table.insert converts, { |
287 | 306 | inp: 'text/moonscript -> (.+)', |
288 | 307 | out: 'text/lua -> %1', |
308 | cost: 2 | |
289 | 309 | transform: single moon.to_lua |
290 | 310 | } |
291 | 311 | else |
292 | 312 | table.insert converts, { |
293 | 313 | inp: 'text/javascript -> (.+)', |
294 | 314 | out: '%1', |
315 | cost: 1 | |
295 | 316 | transform: (source) => |
296 | 317 | f = js.new window.Function, source |
297 | 318 | f! |
314 | 335 | table.insert converts, { |
315 | 336 | inp: 'text/markdown', |
316 | 337 | out: 'text/html+frag', |
338 | cost: 1 | |
317 | 339 | transform: (md) => "<div class=\"markdown\">#{markdown md}</div>" |
318 | 340 | } |
319 | 341 | |
320 | 342 | table.insert converts, { |
321 | 343 | inp: 'text/markdown%+span', |
322 | 344 | out: 'mmm/dom', |
345 | cost: 1 | |
323 | 346 | transform: if MODE == 'SERVER' |
324 | 347 | (source) => |
325 | 348 | html = markdown source |
95 | 95 | |
96 | 96 | __index: (t, k) -> |
97 | 97 | canonical = rawget t, tostring k |
98 | canonical or= Key k | |
98 | -- canonical or= Key k | |
99 | 99 | canonical |
100 | 100 | |
101 | 101 | __newindex: (t, k, v) -> |
108 | 108 | |
109 | 109 | -- get canonical Key instance |
110 | 110 | k = @facet_keys[k] |
111 | return unless k | |
111 | 112 | |
112 | 113 | -- if cached, return |
113 | 114 | if v = rawget t, k |
128 | 129 | __newindex: (t, k, v) -> |
129 | 130 | -- get canonical Key instance |
130 | 131 | k = @facet_keys[k] |
132 | return unless k | |
131 | 133 | |
132 | 134 | rawset t, k, v |
133 | 135 | |
166 | 168 | @facet_keys[copy] = copy |
167 | 169 | |
168 | 170 | _, name = dir_base @path |
169 | @facets['name: alpha'] = name | |
171 | name_key = Key 'name: alpha' | |
172 | @facet_keys[name_key] = name_key | |
173 | @facets[name_key] = name | |
170 | 174 | |
171 | 175 | -- recursively walk to and return the fileder with @path == path |
172 | 176 | -- * path - the path to walk to |
254 | 258 | get: (...) => |
255 | 259 | want = Key ... |
256 | 260 | |
261 | -- return directly if present | |
262 | if val = @facets[want] | |
263 | return val, want | |
264 | ||
257 | 265 | -- find matching key and shortest conversion path |
258 | 266 | key, conversions = @find want |
259 | 267 |
0 | -- a priority queue with an index | |
1 | -- only one element with a given key may exist at a time | |
2 | -- when an element with an existing key is added, | |
3 | -- the element with lower priority survives. | |
4 | class Queue | |
5 | new: => | |
6 | @values = {} | |
7 | @index = {} | |
8 | ||
9 | -- add a value with a given priority to the queue | |
10 | -- if no key is specified, assume the element is uniq | |
11 | add: (value, priority, key) => | |
12 | entry = { :value, :key, :priority } | |
13 | ||
14 | if key | |
15 | if old_entry = @index[key] | |
16 | -- already have an entry for this key | |
17 | -- if it is lower priority, we leave it there and do nothing | |
18 | if old_entry.priority < priority | |
19 | return | |
20 | ||
21 | -- otherwise we remove the old one and continue as normal | |
22 | -- find the index of the old entry | |
23 | local i | |
24 | for ii, entry in ipairs @values | |
25 | if entry == old_entry | |
26 | i = ii | |
27 | break | |
28 | ||
29 | -- remove it | |
30 | table.remove @values, i | |
31 | ||
32 | -- store this entry in the index | |
33 | @index[key] = entry | |
34 | ||
35 | -- store lowest priority last | |
36 | for i, v in ipairs @values | |
37 | if v.priority < priority | |
38 | -- i is the first key that is lower, | |
39 | -- we want to insert right before it | |
40 | table.insert @values, i, entry | |
41 | return | |
42 | ||
43 | -- couldn't find a key with a lower priority, | |
44 | -- so insert at the end | |
45 | table.insert @values, entry | |
46 | ||
47 | peek: => | |
48 | entry = @values[#@values] | |
49 | if entry | |
50 | { :value, :priority, :key } = entry | |
51 | @index[key] = nil if key | |
52 | value, priority | |
53 | ||
54 | pop: => | |
55 | entry = table.remove @values | |
56 | if entry | |
57 | { :value, :priority, :key } = entry | |
58 | @index[key] = nil if key | |
59 | value, priority | |
60 | ||
61 | -- iterator, yields (value, priority), low priority first | |
62 | poll: => @.pop, @ | |
63 | { | |
64 | :Queue | |
65 | } |
0 | import Queue from require 'mmm.mmmfs.queue' | |
1 | ||
2 | describe "Queue", -> | |
3 | it "stores things", -> | |
4 | queue = Queue! | |
5 | queue\add "test", 1 | |
6 | queue\add "toast", 2 | |
7 | queue\add "spice", 3 | |
8 | ||
9 | assert.is.equal "test", queue\pop! | |
10 | assert.is.equal "toast", queue\pop! | |
11 | assert.is.equal "spice", queue\pop! | |
12 | assert.is.nil queue\pop! | |
13 | ||
14 | it "doesnt care about the order", -> | |
15 | queue = Queue! | |
16 | queue\add "spice", 3 | |
17 | queue\add "test", 1 | |
18 | queue\add "toast", 2 | |
19 | ||
20 | assert.is.equal "test", queue\pop! | |
21 | assert.is.equal "toast", queue\pop! | |
22 | ||
23 | queue\add "pepper", 5 | |
24 | queue\add "salt", .5 | |
25 | assert.is.equal "salt", queue\pop! | |
26 | assert.is.equal "spice", queue\pop! | |
27 | assert.is.equal "pepper", queue\pop! | |
28 | ||
29 | it "can be peeked", -> | |
30 | queue = Queue! | |
31 | queue\add "spice", 3 | |
32 | queue\add "test", 1 | |
33 | queue\add "toast", 2 | |
34 | ||
35 | assert.is.equal "test", queue\peek! | |
36 | assert.is.equal "test", queue\pop! | |
37 | queue\pop! | |
38 | ||
39 | queue\add "pepper", 5 | |
40 | queue\add "salt", .5 | |
41 | ||
42 | assert.is.equal "salt", queue\peek! | |
43 | queue\pop! | |
44 | queue\pop! | |
45 | ||
46 | assert.is.equal "pepper", queue\peek! | |
47 | queue\pop! | |
48 | ||
49 | assert.is.nil queue\peek! | |
50 | ||
51 | it "keeps keys in an index", -> | |
52 | queue = Queue! | |
53 | queue\add "test", 1, 'test' | |
54 | queue\add "toast", 2, 'toast' | |
55 | queue\add "spice", 3, 'spice' | |
56 | ||
57 | assert.is.equal "test", queue\peek! | |
58 | queue\add "spice2", .5, 'spice' | |
59 | assert.is.equal "spice2", queue\pop! | |
60 | assert.is.equal "test", queue\pop! | |
61 | ||
62 | queue\add "bad toast", 5, 'toast' | |
63 | assert.is.equal "toast", queue\pop! | |
64 | assert.is.nil queue\pop! | |
65 | ||
66 | it "provides an iterator", -> | |
67 | queue = Queue! | |
68 | queue\add "test", 1 | |
69 | queue\add "spice", 3 | |
70 | queue\add "toast", 2 | |
71 | ||
72 | expect = {'test', 'toast', 'late', 'spice'} | |
73 | expect_next = 1 | |
74 | report = spy.new (v, i) -> | |
75 | assert.is.equal expect[expect_next], v | |
76 | expect_next += 1 | |
77 | ||
78 | for value, prio in queue\poll! | |
79 | report value, prio | |
80 | ||
81 | if value == 'toast' | |
82 | queue\add "late", 0.5 | |
83 | ||
84 | assert.stub(report).was.called_with('test', 1) | |
85 | assert.stub(report).was.called_with('toast', 2) | |
86 | assert.stub(report).was.called_with('spice', 3) | |
87 | assert.stub(report).was.called_with('late', 0.5) |