diff --git a/assets/suggest_tags.js b/assets/suggest_tags.js
new file mode 100644
index 0000000..5ffd823
--- /dev/null
+++ b/assets/suggest_tags.js
@@ -0,0 +1,105 @@
+console.log("Hello, world!");
+
+tag_suggestion_list = {
+ list_element: null,
+ suggestion_elements: [],
+}
+
+var lel = document.createElement('div');
+
+/**
+ * Stolen from medium.com/@jh3y
+ * returns x, y coordinates for absolute positioning of a span within a given text input
+ * at a given selection point
+ * @param {object} input - the input element to obtain coordinates for
+ * @param {number} selectionPoint - the selection point for the input
+ */
+function getCursorXY(input, selectionPoint){
+ const {
+ offsetLeft: inputX,
+ offsetTop: inputY,
+ } = input
+ // create a dummy element that will be a clone of our input
+ const div = document.createElement('div')
+ // get the computed style of the input and clone it onto the dummy element
+ const copyStyle = getComputedStyle(input)
+ for (const prop of copyStyle) {
+ div.style[prop] = copyStyle[prop]
+ }
+ // we need a character that will replace whitespace when filling our dummy element if it's a single line
+ const swap = '.'
+ const inputValue = input.tagName === 'INPUT' ? input.value.replace(/ /g, swap) : input.value
+ // set the div content to that of the textarea up until selection
+ const textContent = inputValue.substr(0, selectionPoint)
+ // set the text content of the dummy element div
+ div.textContent = textContent
+ if (input.tagName === 'TEXTAREA') div.style.height = 'auto'
+ // if a single line input then the div needs to be single line and not break out like a text area
+ if (input.tagName === 'INPUT') div.style.width = 'auto'
+ // create a marker element to obtain caret position
+ const span = document.createElement('span')
+ // give the span the textContent of remaining content so that the recreated dummy element is as close as possible
+ span.textContent = inputValue.substr(selectionPoint) || '.'
+ // append the span marker to the div
+ div.appendChild(span)
+ // append the dummy element to the body
+ document.body.appendChild(div)
+ // get the marker position, this is the caret position top and left relative to the input
+ const { offsetLeft: spanX, offsetTop: spanY } = span
+ // lastly, remove that dummy element
+ // NOTE:: can comment this out for debugging purposes if you want to see where that span is rendered
+ document.body.removeChild(div)
+ // return an object with the x and y of the caret. account for input positioning so that you don't need to wrap the input
+ return {
+ x: inputX + spanX,
+ y: inputY + spanY,
+ }
+}
+
+function display_suggestions(elem, sugg, event){
+ console.log("sugg:",sugg);
+ //Check that the value hasn't change since we fired
+ //off the request
+ recent = elem.value.split(";").pop().trim();
+ if(recent == sugg[0]){
+ var sugx, sugy = getCursorXY(elem,elem.value.length);
+ console.log("Looking at position to display suggestions:",sugx, sugy);
+ for(var i in sugg){
+ console.log("Displaying suggestion:",sugg[i]);
+ lel.setAttribute('style',`left: $(sugx)px; top: $(sugy)px;`);
+ }
+ }
+}
+
+function hint_tags(elem, event){
+ //Get the most recent tag
+ recent = elem.value.split(";").pop().trim();
+ if(recent.length > 0){
+ console.log("Most recent tag:",recent);
+ //Ask the server for tags that look like this
+ xhr = new XMLHttpRequest();
+ xhr.open("GET", "/_api?call=suggest&data=" + recent);
+ xhr.onreadystatechange = function(e){
+ if(xhr.readyState === 4){
+ console.log("Event:",e);
+ suggestions = xhr.response.split(";");
+ console.log("suggestions:",suggestions);
+ display_suggestions(elem,suggestions, event);
+
+ }
+ }
+ xhr.send()
+ }
+}
+
+function init(){
+ tag_el_list = document.getElementsByName("tags");
+ console.assert(tag_el_list.length == 1);
+ tag_el = tag_el_list[0];
+ tag_el.onkeyup = function(event){
+ console.log("Looking at tag:", event);
+ console.log("And element:",tag_el);
+ hint_tags(tag_el, event);
+ }
+}
+document.addEventListener("DOMContentLoaded",init,false);
diff --git a/conf/smr.conf.in b/conf/smr.conf.in
index 526a92a..a896510 100644
--- a/conf/smr.conf.in
+++ b/conf/smr.conf.in
@@ -43,6 +43,7 @@ domain * {
route /_css/milligram.css asset_serve_milligram_css
route /_css/milligram.min.css.map asset_serve_milligram_min_css_map
route /_faq asset_serve_faq_html
+ route /_js/suggest_tags.js asset_serve_suggest_tags_js
route /favicon.ico asset_serve_favicon_ico
route /_paste post_story
route /_edit edit_story
@@ -54,6 +55,7 @@ domain * {
route /_preview preview
route /_search search
route /_archive archive
+ route /_api api
# Leading ^ is needed for dynamic routes, kore says the route is dynamic if it does not start with '/'
route ^/[^_].* read_story
@@ -111,5 +113,8 @@ domain * {
params post ^/_claim {
validate user v_subdomain
}
-
+ params get /_api {
+ validate call v_any
+ validate data v_any
+ }
}
diff --git a/src/lua/endpoints/api_get.lua b/src/lua/endpoints/api_get.lua
new file mode 100644
index 0000000..bc156b8
--- /dev/null
+++ b/src/lua/endpoints/api_get.lua
@@ -0,0 +1,62 @@
+local cache = require("cache")
+local sql = require("lsqlite3")
+local db = require("db")
+local queries = require("queries")
+local util = require("util")
+
+local stmnt_tags_get
+
+local oldconfigure = configure
+function configure(...)
+ stmnt_tags_get = util.sqlassert(db.conn:prepare(queries.select_suggest_tags))
+ return oldconfigure(...)
+end
+
+local function suggest_tags(req,data)
+ print("Suggesting tags!")
+ stmnt_tags_get:bind_names{
+ match = data .. "%"
+ }
+ local err = util.do_sql(stmnt_tags_get)
+ if err == sql.ROW or err == sql.DONE then
+ local tags = {data}
+ for tag in stmnt_tags_get:rows() do
+ print("Found tag:",tag[1])
+ table.insert(tags,tag[1])
+ end
+ stmnt_tags_get:reset()
+ http_response_header(req,"Content-Type","text/plain")
+ http_response(req,200,table.concat(tags,";"))
+ else
+ log(LOG_ALERT,"Failed to get tag suggestions in an unusual way:" .. err .. ":" .. db.conn:errmsg())
+ --This is bad though
+ local page = pages.error({
+ errcode = 500,
+ errcodemsg = "Server error",
+ explanation = string.format(
+ "Failed to retreive tags from database:%d:%q",
+ err,
+ db.conn:errmsg()
+ ),
+ })
+ stmnt_tags_get:reset()
+ http_response(req,500,page)
+ end
+end
+
+local function api_get(req)
+ http_request_populate_qs(req)
+ local call = assert(http_argument_get_string(req,"call"))
+ local data = assert(http_argument_get_string(req,"data"))
+ local body
+ if call == "suggest" then
+ --[[
+ Prevent a malicious user from injecting '%' into the string
+ we're searching for, potentially causing a DoS with a
+ sufficiently backtrack-ey search/tag combination.
+ ]]
+ assert(data:match("^[a-zA-Z0-9,%s-]+$"),"Bad characters in tag")
+ return suggest_tags(req,data)
+ end
+end
+return api_get
diff --git a/src/lua/endpoints/edit_get.lua b/src/lua/endpoints/edit_get.lua
index 1dda582..0517a76 100644
--- a/src/lua/endpoints/edit_get.lua
+++ b/src/lua/endpoints/edit_get.lua
@@ -61,7 +61,10 @@ local function edit_get(req)
story = story_id,
err = "",
tags = tags_txt,
- unlisted = unlisted == 1
+ unlisted = unlisted == 1,
+ extra_load = {
+ ''
+ }
}
http_response(req,200,ret)
end
diff --git a/src/lua/endpoints/paste_get.lua b/src/lua/endpoints/paste_get.lua
index ba6cd49..b178831 100644
--- a/src/lua/endpoints/paste_get.lua
+++ b/src/lua/endpoints/paste_get.lua
@@ -19,6 +19,9 @@ local function paste_get(req)
return assert(pages.paste{
domain = config.domain,
err = "",
+ extra_load = {
+ ''
+ }
})
end)
http_response(req,200,text)
@@ -29,6 +32,9 @@ local function paste_get(req)
user = author,
err = "",
text="",
+ extra_load = {
+ ''
+ }
})
elseif host ~= config.domain and author == nil then
http_response_header(req,"Location",string.format("https://%s/_paste",config.domain))
diff --git a/src/lua/init.lua b/src/lua/init.lua
index cfc28af..61aac35 100644
--- a/src/lua/init.lua
+++ b/src/lua/init.lua
@@ -34,6 +34,7 @@ local endpoint_names = {
claim = {"get","post"},
search = {"get"},
archive = {"get"},
+ api = {"get"},
}
local endpoints = {}
for name, methods in pairs(endpoint_names) do
@@ -151,4 +152,13 @@ function archive(req)
endpoints.archive_get(req)
end
+function api(req)
+ local method = http_method_text(req)
+ if method == "GET" then
+ endpoints.api_get(req)
+ elseif method == "POST" then
+ endpoints.api_post(req)
+ end
+end
+
print("Done with init.lua")
diff --git a/src/pages/edit.etlua.in b/src/pages/edit.etlua.in
index b2f9382..525c2ef 100644
--- a/src/pages/edit.etlua.in
+++ b/src/pages/edit.etlua.in
@@ -41,7 +41,7 @@
-
+
<{cat src/pages/parts/footer.etlua}>
diff --git a/src/pages/parts/header.etlua b/src/pages/parts/header.etlua
index f56c97d..194a606 100644
--- a/src/pages/parts/header.etlua
+++ b/src/pages/parts/header.etlua
@@ -14,6 +14,11 @@
<% end %>
+ <% if extra_load then %>
+ <% for _,load in ipairs(extra_load) do %>
+ <%- load %>
+ <% end %>
+ <% end %>