git.s-ol.nu mmm / deb21aa
new type/pathfinding algorithm s-ol 3 years ago
6 changed file(s) with 242 addition(s) and 26 deletion(s). Raw diff Collapse all Expand all
00 require = relative ..., 1
11 import Key from require '.fileder'
2 import converts, get_conversions, apply_conversions from require '.conversion'
2 import get_conversions, apply_conversions from require '.conversion'
33 import ReactiveVar, get_or_create, text, elements, tohtml from require 'mmm.component'
44 import pre, div, nav, span, button, a, code, select, option from elements
55 import languages from require 'mmm.highlighting'
6 converts = require '.converts'
67
78 keep = (var) ->
89 last = var\get!
1415 {
1516 inp: "text/#{lang}.*",
1617 out: 'mmm/dom',
18 cost: 0
1719 transform: (val) => languages[lang] val
1820 }
1921
2729 {
2830 inp: 'URL.*'
2931 out: 'mmm/dom'
32 cost: 0
3033 transform: (href) => span a (code href), :href
3134 }
3235 }
00 require = relative ..., 1
1 converts = require '.converts'
1 base_converts = require '.converts'
2 import Queue from require '.queue'
23
34 count = (base, pattern='->') -> select 2, base\gsub pattern, ''
45 escape_pattern = (inp) -> "^#{inp\gsub '([^%w])', '%%%1'}$"
56 escape_inp = (inp) -> "^#{inp\gsub '([-/])', '%%%1'}$"
7
8 local print_conversions
69
710 -- attempt to find a conversion path from 'have' to 'want'
811 -- * have - start type string or list of type strings
912 -- * want - stop type pattern
1013 -- * limit - limit conversion amount
1114 -- 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) ->
1316 assert have, 'need starting type(s)'
1417
1518 if 'string' == type have
1821 assert #have > 0, 'need starting type(s) (list was empty)'
1922
2023 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]
2325
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
2957 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
4159
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})"
4473
4574 -- apply transforms for conversion path sequentially
4675 -- * conversions - conversions from get_conversions
72101 apply_conversions conversions, value, ...
73102
74103 {
75 :converts
76104 :get_conversions
77105 :apply_conversions
78106 :convert
2929 {
3030 inp: "text/#{lang}",
3131 out: 'mmm/dom',
32 cost: 5
3233 transform: (val) => pre languages[lang] val
3334 }
3435
4142 {
4243 inp: 'fn -> (.+)',
4344 out: '%1',
45 cost: 1
4446 transform: (val, fileder) => val fileder
4547 }
4648 {
4749 inp: 'mmm/component',
4850 out: 'mmm/dom',
51 const: 3
4952 transform: single tohtml
5053 }
5154 {
5255 inp: 'mmm/dom',
5356 out: 'text/html+frag',
57 cost: 3
5458 transform: (node) => if MODE == 'SERVER' then node else node.outerHTML
5559 }
5660 {
5862 -- @TODO: this doesn't feel right... maybe mmm/dom has to go?
5963 inp: 'mmm/dom',
6064 out: 'text/html',
65 cost: 3
6166 transform: (html, fileder) => render html, fileder
6267 }
6368 {
6469 inp: 'text/html%+frag',
6570 out: 'mmm/dom',
71 cost: 1
6672 transform: if MODE == 'SERVER'
6773 (html, fileder) =>
6874 html = html\gsub '<mmm%-link%s+(.-)>(.-)</mmm%-link>', (attrs, text) ->
137143 {
138144 inp: 'text/lua -> (.+)',
139145 out: '%1',
146 cost: 0.5
140147 transform: loadwith load or loadstring
141148 }
142149 {
143150 inp: 'mmm/tpl -> (.+)',
144151 out: '%1',
152 cost: 1
145153 transform: (source, fileder) =>
146154 source\gsub '{{(.-)}}', (expr) ->
147155 path, facet = expr\match '^([%w%-_%./]*)%+(.*)'
152160 {
153161 inp: 'time/iso8601-date',
154162 out: 'time/unix',
163 cost: 0.5
155164 transform: (val) =>
156165 year, _, month, day = val\match '^%s*(%d%d%d%d)(%-?)([01]%d)%2([0-3]%d)%s*$'
157166 assert year, "failed to parse ISO 8601 date: '#{val}'"
160169 {
161170 inp: 'URL -> twitter/tweet',
162171 out: 'mmm/dom',
172 cost: 1
163173 transform: (href) =>
164174 id = assert (href\match 'twitter.com/[^/]-/status/(%d*)'), "couldn't parse twitter/tweet URL: '#{href}'"
165175 if MODE == 'CLIENT'
175185 {
176186 inp: 'URL -> youtube/video',
177187 out: 'mmm/dom',
188 cost: 1
178189 transform: (link) =>
179190 id = link\match 'youtu%.be/([^/]+)'
180191 id or= link\match 'youtube.com/watch.*[?&]v=([^&]+)'
195206 {
196207 inp: 'URL -> image/.+',
197208 out: 'mmm/dom',
209 cost: 1
198210 transform: (src, fileder) => img :src
199211 }
200212 {
201213 inp: 'URL -> video/.+',
202214 out: 'mmm/dom',
215 cost: 1
203216 transform: (src) =>
204217 -- @TODO: add parsed MIME type
205218 video (source :src), controls: true, loop: true
207220 {
208221 inp: 'text/plain',
209222 out: 'mmm/dom',
223 cost: 2
210224 transform: (val) => span val
211225 }
212226 {
213227 inp: 'alpha',
214228 out: 'mmm/dom',
229 cost: 2
215230 transform: single code
216231 }
217232 -- this one needs a higher cost
223238 {
224239 inp: '(.+)',
225240 out: 'URL -> %1',
241 cost: 10
226242 transform: (_, fileder, key) => "#{fileder.path}/#{key.name}:#{@from}"
227243 }
228244 {
229245 inp: 'table',
230246 out: 'text/json',
247 cost: 2
231248 transform: do
232249 tojson = (obj) ->
233250 switch type obj
252269 {
253270 inp: 'table',
254271 out: 'mmm/dom',
272 cost: 5
255273 transform: do
256274 deep_tostring = (tbl, space='') ->
257275 buf = space .. tostring tbl
280298 table.insert converts, {
281299 inp: 'text/moonscript -> (.+)',
282300 out: '%1',
301 cost: 1
283302 transform: loadwith moon.load or moon.loadstring
284303 }
285304
286305 table.insert converts, {
287306 inp: 'text/moonscript -> (.+)',
288307 out: 'text/lua -> %1',
308 cost: 2
289309 transform: single moon.to_lua
290310 }
291311 else
292312 table.insert converts, {
293313 inp: 'text/javascript -> (.+)',
294314 out: '%1',
315 cost: 1
295316 transform: (source) =>
296317 f = js.new window.Function, source
297318 f!
314335 table.insert converts, {
315336 inp: 'text/markdown',
316337 out: 'text/html+frag',
338 cost: 1
317339 transform: (md) => "<div class=\"markdown\">#{markdown md}</div>"
318340 }
319341
320342 table.insert converts, {
321343 inp: 'text/markdown%+span',
322344 out: 'mmm/dom',
345 cost: 1
323346 transform: if MODE == 'SERVER'
324347 (source) =>
325348 html = markdown source
9595
9696 __index: (t, k) ->
9797 canonical = rawget t, tostring k
98 canonical or= Key k
98 -- canonical or= Key k
9999 canonical
100100
101101 __newindex: (t, k, v) ->
108108
109109 -- get canonical Key instance
110110 k = @facet_keys[k]
111 return unless k
111112
112113 -- if cached, return
113114 if v = rawget t, k
128129 __newindex: (t, k, v) ->
129130 -- get canonical Key instance
130131 k = @facet_keys[k]
132 return unless k
131133
132134 rawset t, k, v
133135
166168 @facet_keys[copy] = copy
167169
168170 _, 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
170174
171175 -- recursively walk to and return the fileder with @path == path
172176 -- * path - the path to walk to
254258 get: (...) =>
255259 want = Key ...
256260
261 -- return directly if present
262 if val = @facets[want]
263 return val, want
264
257265 -- find matching key and shortest conversion path
258266 key, conversions = @find want
259267
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)