M fennel.css => fennel.css +52 -4
@@ 15,7 15,7 @@ h1 img { max-width: 1.5em; height: auto; }
a { color: #0d98ba; }
a:hover { color: #0c7090; }
-tt, pre, code, kbd { font-family: "Fira Mono", Inconsolata, monospace; }
+tt, pre, code, kbd, textarea { font-family: "Fira Mono", Inconsolata, monospace; }
#where li { display: inline; margin: 2em; }
@@ 105,14 105,62 @@ tt, pre, code, kbd { font-family: "Fira Mono", Inconsolata, monospace; }
#sample .comment {/* font-lock-comment-face */ color: #b22222;}
/* see fennel */
-#see-fennel textarea {
+#see-fennel .editor {
+ display: grid; /* This combined with `grid-column: 1` and `grid-row: 1` gives overlapped elements */
float: left;
- width: 48%;
- height: 25em;
+ width: calc(50% - 1em);
+ height: 20em;
margin: 0.5em;
+}
+
+#see-fennel .editor textarea,
+#see-fennel .editor pre {
+ box-sizing: border-box;
+ font-size: 13px;
+ grid-column: 1;
+ grid-row: 1;
+ height: 100%;
+ line-height: 1.5;
+ margin: 0;
+ overflow-y: auto;
padding: 0.4em;
+ white-space: pre-wrap;
+ width: 100%;
}
+#see-fennel .editor textarea {
+ border: 1px solid transparent; /* To make dimensions match with pre */
+ background: transparent;
+ caret-color: #444; /* TODO: Somehow inherit the color from body */
+ color: transparent;
+ outline: none; /* No focus glow */
+ resize: none;
+ z-index: 1;
+}
+
+#see-fennel .editor textarea::placeholder {
+ opacity: 1;
+ color: #94b996; /* Same color as comment */
+}
+
+#see-fennel .editor pre {
+ border-radius: 3px;
+ border: 1px solid #ddd;
+ z-index: 0;
+}
+
+/* Syntax highlight theme */
+#see-fennel .editor pre { background-color: #f9fef5; color: #181a19; }
+#see-fennel .editor pre .comment { color: #94b996; font-style: italic; }
+#see-fennel .editor pre .keyword,
+#see-fennel .editor pre .string,
+#see-fennel .editor pre .number,
+#see-fennel .editor pre .nil,
+#see-fennel .editor pre .boolean { color: #8e9144; }
+#see-fennel .editor pre .symbol.special-symbol,
+#see-fennel .editor pre .symbol.macro-symbol,
+#see-fennel .editor pre .symbol.global-symbol { color: #448f22; font-weight: bold; }
+
#see-fennel button, #see-fennel select {
margin: 0.5em;
width: 48%;
A highlight.lua => highlight.lua +91 -0
@@ 0,0 1,91 @@
+local function scan(text)
+ local current_index = 1
+ local last_matching_at = 0
+ local text_length = string.len(text)
+ local function at_eof_3f()
+ return (current_index > text_length)
+ end
+ local function yield_buffered_nonmatching()
+ if (0 < (current_index - last_matching_at)) then
+ return coroutine.yield({kind = nil, value = string.sub(text, (last_matching_at + 1), (current_index - 1))})
+ else
+ return nil
+ end
+ end
+ local function yield(kind, matched)
+ yield_buffered_nonmatching()
+ coroutine.yield({kind = kind, value = matched})
+ current_index = (current_index + string.len(matched))
+ last_matching_at = (current_index - 1)
+ return nil
+ end
+ local function attempt_match(kind, pattern)
+ local _2_ = string.match(text, pattern, current_index)
+ if (nil ~= _2_) then
+ local matched = _2_
+ yield(kind, matched)
+ return true
+ else
+ return nil
+ end
+ end
+ local function increment_current_index()
+ current_index = (current_index + 1)
+ return nil
+ end
+ local symbol_char = "!%$&#%*%+%-%./:<=>%?%^_%w"
+ while not at_eof_3f() do
+ do local _ = (attempt_match("comment", "^(;[^\n]*[\n])") or attempt_match("string", "^(\"[^\"]*\")") or attempt_match("keyword", ("^(:[" .. symbol_char .. "]+)")) or attempt_match("number", "^([%+%-]?%d+[xX]?%d*%.?%d?)") or attempt_match("nil", ("^(nil)[^" .. symbol_char .. "]")) or attempt_match("boolean", ("^(true)[^" .. symbol_char .. "]")) or attempt_match("boolean", ("^(false)[^" .. symbol_char .. "]")) or attempt_match("symbol", ("^([" .. symbol_char .. "]+)")) or attempt_match("bracket", "^([%(%)%[%]{}])") or increment_current_index()) end
+ end
+ return yield_buffered_nonmatching()
+end
+local function with_symbol_subkind(syntax, _4_)
+ local _arg_5_ = _4_
+ local token = _arg_5_
+ local kind = _arg_5_["kind"]
+ local value = _arg_5_["value"]
+ if ("symbol" == kind) then
+ local _6_ = syntax[value]
+ if ((_G.type(_6_) == "table") and ((_6_)["special?"] == true)) then
+ token.subkind = "special-symbol"
+ elseif ((_G.type(_6_) == "table") and ((_6_)["macro?"] == true)) then
+ token.subkind = "macro-symbol"
+ elseif ((_G.type(_6_) == "table") and ((_6_)["global?"] == true)) then
+ token.subkind = "global-symbol"
+ else
+ token.subkind = nil
+ end
+ else
+ end
+ return token
+end
+local function token__3ehtml(_9_)
+ local _arg_10_ = _9_
+ local kind = _arg_10_["kind"]
+ local subkind = _arg_10_["subkind"]
+ local value = _arg_10_["value"]
+ local class = ((kind or "nonmatching") .. " " .. (subkind or ""))
+ local escaped_value = string.gsub(string.gsub(value, "&", "&"), "<", "<")
+ return ("<span class=\"" .. class .. "\">" .. escaped_value .. "</span>")
+end
+local function for_html(syntax, code)
+ local tokens
+ do
+ local tbl_17_auto = {}
+ local i_18_auto = #tbl_17_auto
+ local function _11_()
+ return scan((code .. "\n"))
+ end
+ for token in coroutine.wrap(_11_) do
+ local val_19_auto = token__3ehtml(with_symbol_subkind(syntax, token))
+ if (nil ~= val_19_auto) then
+ i_18_auto = (i_18_auto + 1)
+ do end (tbl_17_auto)[i_18_auto] = val_19_auto
+ else
+ end
+ end
+ tokens = tbl_17_auto
+ end
+ return table.concat(tokens)
+end
+return {["for-html"] = for_html, for_html = for_html}
M see.html => see.html +11 -2
@@ 11,8 11,17 @@
<body>
<div id="see-fennel">
<h1>See Fennel</h1>
- <textarea id="fennel-source" placeholder="Enter some Fennel code to see it compiled to Lua!"></textarea>
- <textarea id="lua-source" placeholder="Or enter some Lua to see it compiled to Fennel..."></textarea>
+
+ <div class="editor">
+ <textarea id="fennel-source" placeholder="Enter some Fennel code to see it compiled to Lua!"></textarea>
+ <pre aria-hidden="true" id="fennel-highlighted"></pre>
+ </div>
+
+ <div class="editor">
+ <textarea id="lua-source" placeholder="Or enter some Lua to see it compiled to Fennel..."></textarea>
+ <pre aria-hidden="true" id="lua-highlighted"></pre>
+ </div>
+
<button id="compile-fennel">→</button>
<button id="compile-lua">←</button>
<select id="sample-fennel"><option>Fennel examples...</option></select>
M see.lua => see.lua +70 -10
@@ 1,6 1,9 @@
package.path = "./?.lua"
local js = require("js")
+local fennel = require("fennel/fennel")
+local highlight = require("highlight")
+
-- minimal shims just to allow the compilers to load in Fengari
package.loaded.ffi = {typeof=function() end}
os = {getenv=function() end}
@@ 17,7 20,10 @@ local compile_lua = document:getElementById("compile-lua")
local out = document:getElementById("out")
local fennel_source = document:getElementById("fennel-source")
+local fennel_highlighted = document:getElementById("fennel-highlighted")
+
local lua_source = document:getElementById("lua-source")
+local lua_highlighted = document:getElementById("lua-highlighted")
local status = function(msg, success)
out.innerHTML = msg
@@ 41,6 47,60 @@ lua_source.onkeydown = function(_,e)
end
end
+-- Syntax highlighting is adapted from this guide: https://css-tricks.com/creating-an-editable-textarea-that-supports-syntax-highlighted-code/
+--
+-- TL;DR: To highlight syntax in a user-editable textarea, we
+-- 1. Copy the contents of the textarea on every edit
+-- 2. Run the contents through a highlighting function to get styled HTML
+-- 3. Set the HTML into a pre element that's just behind the textarea
+-- 4. Make the textarea completely transparent except for the input caret
+-- and ensure the input textarea and output pre have the same dimensions
+-- using a bunch of CSS
+
+local syntax = fennel.syntax()
+function highlight_fennel()
+ fennel_highlighted.innerHTML = highlight.for_html(syntax, fennel_source.value)
+end
+
+function highlight_lua()
+ -- TODO: Implement :-)
+ lua_highlighted.innerHTML = lua_source.value
+end
+
+-- Initial run
+highlight_fennel()
+highlight_lua()
+
+fennel_source.onscroll = function()
+ fennel_highlighted.scrollTop = fennel_source.scrollTop
+ fennel_highlighted.scrollLeft = fennel_source.scrollLeft
+end
+
+lua_source.onscroll = function()
+ lua_highlighted.scrollTop = lua_source.scrollTop
+ lua_highlighted.scrollLeft = lua_source.scrollLeft
+end
+
+fennel_source.oninput = function()
+ highlight_fennel()
+ fennel_source.onscroll()
+end
+
+lua_source.oninput = function()
+ highlight_lua()
+ lua_source.onscroll()
+end
+
+function update_fennel_source(new_value)
+ fennel_source.value = new_value
+ highlight_fennel()
+end
+
+function update_lua_source(new_value)
+ lua_source.value = new_value
+ highlight_lua()
+end
+
local anti_msg = "Compiled Lua to Fennel.\n\n"..
"Note that compiling Lua to Fennel can result in some" ..
" strange-looking code when\nusing constructs that Fennel" ..
@@ 66,10 126,10 @@ local init_worker = function(auto_click)
-- because we can't send tables as events, we encode the type of the
-- message in the last character of the string.
if event.data:match(" $") then
- lua_source.value = event.data
+ update_lua_source(event.data)
status("Compiled Fennel to Lua.", true)
elseif event.data:match("\t$") then
- fennel_source.value = event.data
+ update_fennel_source(event.data)
status(anti_msg, true)
else
status(event.data, false)
@@ 85,7 145,7 @@ local load_direct = function(auto_click)
compile_fennel.onclick = function()
local ok, code = pcall(fennel.compileString, fennel_source.value)
if ok then
- lua_source.value = code
+ update_lua_source(code)
status("Compiled Fennel to Lua.", true)
else
status(tostring(code), false)
@@ 99,7 159,7 @@ local load_direct = function(auto_click)
local ok, code = pcall(antifennel, lua_source.value)
if ok then
- fennel_source.value = code
+ update_fennel_source(code)
status(anti_msg, true)
else
status(tostring(code), false)
@@ 224,7 284,7 @@ end",
end\
end"}
-local init_samples = function(id, samples, target)
+local init_samples = function(id, samples, update_source)
local select = document:getElementById(id)
for name, sample in pairs(samples) do
local option = document:createElement("option")
@@ 235,12 295,12 @@ local init_samples = function(id, samples, target)
select.onchange = function(self, e)
init()
local code = samples[self.value]
- if code then target.value = code end
+ if code then update_source(code) end
end
end
-init_samples("sample-fennel", fennel_samples, fennel_source)
-init_samples("sample-lua", lua_samples, lua_source)
+init_samples("sample-fennel", fennel_samples, update_fennel_source)
+init_samples("sample-lua", lua_samples, update_lua_source)
if js.global.URLSearchParams then
local params = js.new(js.global.URLSearchParams, document.location.search)
@@ 248,10 308,10 @@ if js.global.URLSearchParams then
local lua_param = params:get("lua")
if tostring(fennel_param) ~= "null" then
- fennel_source.value = fennel_param
+ update_fennel_source(fennel_param)
init(compile_fennel)
elseif tostring(lua_param) ~= "null" then
- lua_source.value = lua_param
+ update_lua_source(lua_param)
init(compile_lua)
end
end