aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authors-ol <s-ol@users.noreply.github.com>2019-10-14 13:07:27 +0000
committers-ol <s-ol@users.noreply.github.com>2019-10-14 13:07:27 +0000
commitdeb21aa43fe8bf11eb276803973b272913b7e716 (patch)
tree1717990ecbb28936dc498e8cee94a00593332c0c
parentfix browser nav issues, fs default sorting (diff)
downloadmmm-deb21aa43fe8bf11eb276803973b272913b7e716.tar.gz
mmm-deb21aa43fe8bf11eb276803973b272913b7e716.zip
new type/pathfinding algorithm
-rw-r--r--mmm/mmmfs/browser.moon5
-rw-r--r--mmm/mmmfs/conversion.moon78
-rw-r--r--mmm/mmmfs/converts.moon23
-rw-r--r--mmm/mmmfs/fileder.moon12
-rw-r--r--mmm/mmmfs/queue.moon66
-rw-r--r--spec/queue_spec.moon88
6 files changed, 244 insertions, 28 deletions
diff --git a/mmm/mmmfs/browser.moon b/mmm/mmmfs/browser.moon
index cdeba30..f872ed9 100644
--- a/mmm/mmmfs/browser.moon
+++ b/mmm/mmmfs/browser.moon
@@ -1,9 +1,10 @@
require = relative ..., 1
import Key from require '.fileder'
-import converts, get_conversions, apply_conversions from require '.conversion'
+import get_conversions, apply_conversions from require '.conversion'
import ReactiveVar, get_or_create, text, elements, tohtml from require 'mmm.component'
import pre, div, nav, span, button, a, code, select, option from elements
import languages from require 'mmm.highlighting'
+converts = require '.converts'
keep = (var) ->
last = var\get!
@@ -15,6 +16,7 @@ code_cast = (lang) ->
{
inp: "text/#{lang}.*",
out: 'mmm/dom',
+ cost: 0
transform: (val) => languages[lang] val
}
@@ -28,6 +30,7 @@ casts = {
{
inp: 'URL.*'
out: 'mmm/dom'
+ cost: 0
transform: (href) => span a (code href), :href
}
}
diff --git a/mmm/mmmfs/conversion.moon b/mmm/mmmfs/conversion.moon
index 31cbf5e..4b47bba 100644
--- a/mmm/mmmfs/conversion.moon
+++ b/mmm/mmmfs/conversion.moon
@@ -1,16 +1,19 @@
require = relative ..., 1
-converts = require '.converts'
+base_converts = require '.converts'
+import Queue from require '.queue'
count = (base, pattern='->') -> select 2, base\gsub pattern, ''
escape_pattern = (inp) -> "^#{inp\gsub '([^%w])', '%%%1'}$"
escape_inp = (inp) -> "^#{inp\gsub '([-/])', '%%%1'}$"
+local print_conversions
+
-- attempt to find a conversion path from 'have' to 'want'
-- * have - start type string or list of type strings
-- * want - stop type pattern
-- * limit - limit conversion amount
-- returns a list of conversion steps
-get_conversions = (want, have, _converts=converts, limit=5) ->
+get_conversions = (want, have, converts=base_converts, limit=5) ->
assert have, 'need starting type(s)'
if 'string' == type have
@@ -19,29 +22,55 @@ get_conversions = (want, have, _converts=converts, limit=5) ->
assert #have > 0, 'need starting type(s) (list was empty)'
want = escape_pattern want
- iterations = limit + math.max table.unpack [count type for type in *have]
- have = [{ :start, rest: start, conversions: {} } for start in *have]
-
- for i=1, iterations
- next_have, c = {}, 1
- for { :start, :rest, :conversions } in *have
- if rest\match want
- return conversions, start
+ limit = limit + 3 * math.max table.unpack [count type for type in *have]
+
+ had = {}
+ queue = Queue!
+ for start in *have
+ return {}, start if want\match start
+ queue\add { :start, rest: start, conversions: {} }, 0, start
+
+ best = Queue!
+
+ while true
+ entry, cost = queue\pop!
+ if not entry or cost > limit
+ break
+
+ { :start, :rest, :conversions } = entry
+ had[rest] = true
+ for convert in *converts
+ inp = escape_inp convert.inp
+ continue unless rest\match inp
+ result = rest\gsub inp, convert.out
+ continue unless result
+ continue if had[result]
+
+ next_entry = {
+ :start
+ rest: result
+ cost: cost + convert.cost
+ conversions: { { :convert, from: rest, to: result }, table.unpack conversions }
+ }
+
+ if result\match want
+ best\add next_entry, next_entry.cost
else
- for convert in *_converts
- inp = escape_inp convert.inp
- continue unless rest\match inp
- result = rest\gsub inp, convert.out
- if result
- next_have[c] = {
- :start,
- rest: result,
- conversions: { { :convert, from: rest, to: result }, table.unpack conversions }
- }
- c += 1
-
- have = next_have
- return unless #have > 0
+ queue\add next_entry, next_entry.cost, result
+
+
+ if solution = best\pop!
+ -- print "BEST: (#{solution.cost})"
+ -- print_conversions solution.conversions
+ solution.conversions, solution.start if solution
+
+-- stringify conversions for debugging
+-- * conversions - conversions from get_conversions
+print_conversions = (conversions) ->
+ print "converting:"
+ for i=#conversions,1,-1
+ step = conversions[i]
+ print "- #{step.from} -> #{step.to} (#{step.convert.cost})"
-- apply transforms for conversion path sequentially
-- * conversions - conversions from get_conversions
@@ -73,7 +102,6 @@ convert = (have, want, value, ...) ->
apply_conversions conversions, value, ...
{
- :converts
:get_conversions
:apply_conversions
:convert
diff --git a/mmm/mmmfs/converts.moon b/mmm/mmmfs/converts.moon
index 9276c33..e4a6ddf 100644
--- a/mmm/mmmfs/converts.moon
+++ b/mmm/mmmfs/converts.moon
@@ -30,6 +30,7 @@ code_hl = (lang) ->
{
inp: "text/#{lang}",
out: 'mmm/dom',
+ cost: 5
transform: (val) => pre languages[lang] val
}
@@ -42,16 +43,19 @@ converts = {
{
inp: 'fn -> (.+)',
out: '%1',
+ cost: 1
transform: (val, fileder) => val fileder
}
{
inp: 'mmm/component',
out: 'mmm/dom',
+ const: 3
transform: single tohtml
}
{
inp: 'mmm/dom',
out: 'text/html+frag',
+ cost: 3
transform: (node) => if MODE == 'SERVER' then node else node.outerHTML
}
{
@@ -59,11 +63,13 @@ converts = {
-- @TODO: this doesn't feel right... maybe mmm/dom has to go?
inp: 'mmm/dom',
out: 'text/html',
+ cost: 3
transform: (html, fileder) => render html, fileder
}
{
inp: 'text/html%+frag',
out: 'mmm/dom',
+ cost: 1
transform: if MODE == 'SERVER'
(html, fileder) =>
html = html\gsub '<mmm%-link%s+(.-)>(.-)</mmm%-link>', (attrs, text) ->
@@ -138,11 +144,13 @@ converts = {
{
inp: 'text/lua -> (.+)',
out: '%1',
+ cost: 0.5
transform: loadwith load or loadstring
}
{
inp: 'mmm/tpl -> (.+)',
out: '%1',
+ cost: 1
transform: (source, fileder) =>
source\gsub '{{(.-)}}', (expr) ->
path, facet = expr\match '^([%w%-_%./]*)%+(.*)'
@@ -153,6 +161,7 @@ converts = {
{
inp: 'time/iso8601-date',
out: 'time/unix',
+ cost: 0.5
transform: (val) =>
year, _, month, day = val\match '^%s*(%d%d%d%d)(%-?)([01]%d)%2([0-3]%d)%s*$'
assert year, "failed to parse ISO 8601 date: '#{val}'"
@@ -161,6 +170,7 @@ converts = {
{
inp: 'URL -> twitter/tweet',
out: 'mmm/dom',
+ cost: 1
transform: (href) =>
id = assert (href\match 'twitter.com/[^/]-/status/(%d*)'), "couldn't parse twitter/tweet URL: '#{href}'"
if MODE == 'CLIENT'
@@ -176,6 +186,7 @@ converts = {
{
inp: 'URL -> youtube/video',
out: 'mmm/dom',
+ cost: 1
transform: (link) =>
id = link\match 'youtu%.be/([^/]+)'
id or= link\match 'youtube.com/watch.*[?&]v=([^&]+)'
@@ -196,11 +207,13 @@ converts = {
{
inp: 'URL -> image/.+',
out: 'mmm/dom',
+ cost: 1
transform: (src, fileder) => img :src
}
{
inp: 'URL -> video/.+',
out: 'mmm/dom',
+ cost: 1
transform: (src) =>
-- @TODO: add parsed MIME type
video (source :src), controls: true, loop: true
@@ -208,11 +221,13 @@ converts = {
{
inp: 'text/plain',
out: 'mmm/dom',
+ cost: 2
transform: (val) => span val
}
{
inp: 'alpha',
out: 'mmm/dom',
+ cost: 2
transform: single code
}
-- this one needs a higher cost
@@ -224,11 +239,13 @@ converts = {
{
inp: '(.+)',
out: 'URL -> %1',
+ cost: 10
transform: (_, fileder, key) => "#{fileder.path}/#{key.name}:#{@from}"
}
{
inp: 'table',
out: 'text/json',
+ cost: 2
transform: do
tojson = (obj) ->
switch type obj
@@ -253,6 +270,7 @@ converts = {
{
inp: 'table',
out: 'mmm/dom',
+ cost: 5
transform: do
deep_tostring = (tbl, space='') ->
buf = space .. tostring tbl
@@ -281,18 +299,21 @@ if MODE == 'SERVER'
table.insert converts, {
inp: 'text/moonscript -> (.+)',
out: '%1',
+ cost: 1
transform: loadwith moon.load or moon.loadstring
}
table.insert converts, {
inp: 'text/moonscript -> (.+)',
out: 'text/lua -> %1',
+ cost: 2
transform: single moon.to_lua
}
else
table.insert converts, {
inp: 'text/javascript -> (.+)',
out: '%1',
+ cost: 1
transform: (source) =>
f = js.new window.Function, source
f!
@@ -315,12 +336,14 @@ do
table.insert converts, {
inp: 'text/markdown',
out: 'text/html+frag',
+ cost: 1
transform: (md) => "<div class=\"markdown\">#{markdown md}</div>"
}
table.insert converts, {
inp: 'text/markdown%+span',
out: 'mmm/dom',
+ cost: 1
transform: if MODE == 'SERVER'
(source) =>
html = markdown source
diff --git a/mmm/mmmfs/fileder.moon b/mmm/mmmfs/fileder.moon
index cf517a1..cdfc649 100644
--- a/mmm/mmmfs/fileder.moon
+++ b/mmm/mmmfs/fileder.moon
@@ -96,7 +96,7 @@ class Fileder
__index: (t, k) ->
canonical = rawget t, tostring k
- canonical or= Key k
+ -- canonical or= Key k
canonical
__newindex: (t, k, v) ->
@@ -109,6 +109,7 @@ class Fileder
-- get canonical Key instance
k = @facet_keys[k]
+ return unless k
-- if cached, return
if v = rawget t, k
@@ -129,6 +130,7 @@ class Fileder
__newindex: (t, k, v) ->
-- get canonical Key instance
k = @facet_keys[k]
+ return unless k
rawset t, k, v
@@ -167,7 +169,9 @@ class Fileder
@facet_keys[copy] = copy
_, name = dir_base @path
- @facets['name: alpha'] = name
+ name_key = Key 'name: alpha'
+ @facet_keys[name_key] = name_key
+ @facets[name_key] = name
-- recursively walk to and return the fileder with @path == path
-- * path - the path to walk to
@@ -255,6 +259,10 @@ class Fileder
get: (...) =>
want = Key ...
+ -- return directly if present
+ if val = @facets[want]
+ return val, want
+
-- find matching key and shortest conversion path
key, conversions = @find want
diff --git a/mmm/mmmfs/queue.moon b/mmm/mmmfs/queue.moon
new file mode 100644
index 0000000..f90d41b
--- /dev/null
+++ b/mmm/mmmfs/queue.moon
@@ -0,0 +1,66 @@
+-- a priority queue with an index
+-- only one element with a given key may exist at a time
+-- when an element with an existing key is added,
+-- the element with lower priority survives.
+class Queue
+ new: =>
+ @values = {}
+ @index = {}
+
+ -- add a value with a given priority to the queue
+ -- if no key is specified, assume the element is uniq
+ add: (value, priority, key) =>
+ entry = { :value, :key, :priority }
+
+ if key
+ if old_entry = @index[key]
+ -- already have an entry for this key
+ -- if it is lower priority, we leave it there and do nothing
+ if old_entry.priority < priority
+ return
+
+ -- otherwise we remove the old one and continue as normal
+ -- find the index of the old entry
+ local i
+ for ii, entry in ipairs @values
+ if entry == old_entry
+ i = ii
+ break
+
+ -- remove it
+ table.remove @values, i
+
+ -- store this entry in the index
+ @index[key] = entry
+
+ -- store lowest priority last
+ for i, v in ipairs @values
+ if v.priority < priority
+ -- i is the first key that is lower,
+ -- we want to insert right before it
+ table.insert @values, i, entry
+ return
+
+ -- couldn't find a key with a lower priority,
+ -- so insert at the end
+ table.insert @values, entry
+
+ peek: =>
+ entry = @values[#@values]
+ if entry
+ { :value, :priority, :key } = entry
+ @index[key] = nil if key
+ value, priority
+
+ pop: =>
+ entry = table.remove @values
+ if entry
+ { :value, :priority, :key } = entry
+ @index[key] = nil if key
+ value, priority
+
+ -- iterator, yields (value, priority), low priority first
+ poll: => @.pop, @
+{
+ :Queue
+}
diff --git a/spec/queue_spec.moon b/spec/queue_spec.moon
new file mode 100644
index 0000000..d095398
--- /dev/null
+++ b/spec/queue_spec.moon
@@ -0,0 +1,88 @@
+import Queue from require 'mmm.mmmfs.queue'
+
+describe "Queue", ->
+ it "stores things", ->
+ queue = Queue!
+ queue\add "test", 1
+ queue\add "toast", 2
+ queue\add "spice", 3
+
+ assert.is.equal "test", queue\pop!
+ assert.is.equal "toast", queue\pop!
+ assert.is.equal "spice", queue\pop!
+ assert.is.nil queue\pop!
+
+ it "doesnt care about the order", ->
+ queue = Queue!
+ queue\add "spice", 3
+ queue\add "test", 1
+ queue\add "toast", 2
+
+ assert.is.equal "test", queue\pop!
+ assert.is.equal "toast", queue\pop!
+
+ queue\add "pepper", 5
+ queue\add "salt", .5
+ assert.is.equal "salt", queue\pop!
+ assert.is.equal "spice", queue\pop!
+ assert.is.equal "pepper", queue\pop!
+
+ it "can be peeked", ->
+ queue = Queue!
+ queue\add "spice", 3
+ queue\add "test", 1
+ queue\add "toast", 2
+
+ assert.is.equal "test", queue\peek!
+ assert.is.equal "test", queue\pop!
+ queue\pop!
+
+ queue\add "pepper", 5
+ queue\add "salt", .5
+
+ assert.is.equal "salt", queue\peek!
+ queue\pop!
+ queue\pop!
+
+ assert.is.equal "pepper", queue\peek!
+ queue\pop!
+
+ assert.is.nil queue\peek!
+
+ it "keeps keys in an index", ->
+ queue = Queue!
+ queue\add "test", 1, 'test'
+ queue\add "toast", 2, 'toast'
+ queue\add "spice", 3, 'spice'
+
+ assert.is.equal "test", queue\peek!
+ queue\add "spice2", .5, 'spice'
+ assert.is.equal "spice2", queue\pop!
+ assert.is.equal "test", queue\pop!
+
+ queue\add "bad toast", 5, 'toast'
+ assert.is.equal "toast", queue\pop!
+ assert.is.nil queue\pop!
+
+ it "provides an iterator", ->
+ queue = Queue!
+ queue\add "test", 1
+ queue\add "spice", 3
+ queue\add "toast", 2
+
+ expect = {'test', 'toast', 'late', 'spice'}
+ expect_next = 1
+ report = spy.new (v, i) ->
+ assert.is.equal expect[expect_next], v
+ expect_next += 1
+
+ for value, prio in queue\poll!
+ report value, prio
+
+ if value == 'toast'
+ queue\add "late", 0.5
+
+ assert.stub(report).was.called_with('test', 1)
+ assert.stub(report).was.called_with('toast', 2)
+ assert.stub(report).was.called_with('spice', 3)
+ assert.stub(report).was.called_with('late', 0.5)