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 %>
diff --git a/src/pages/paste.etlua.in b/src/pages/paste.etlua.in index 1f693d2..65b6447 100644 --- a/src/pages/paste.etlua.in +++ b/src/pages/paste.etlua.in @@ -1,4 +1,4 @@ -<{system cat src/pages/parts/header.etlua}> +<{system cat src/pages/parts/header.etlua}>

Paste

diff --git a/src/smr.c b/src/smr.c index df01ad4..e477952 100644 --- a/src/smr.c +++ b/src/smr.c @@ -26,6 +26,7 @@ int download(struct http_request *); int preview(struct http_request *); int search(struct http_request *); int archive(struct http_request *); +int api(struct http_request *); int style(struct http_request *); int miligram(struct http_request *); int do_lua(struct http_request *req, const char *name); @@ -179,6 +180,12 @@ archive(struct http_request *req){ return do_lua(req,"archive"); } +int +api(struct http_request *req){ + printf("Api call!\n"); + return do_lua(req,"api"); +} + int home(struct http_request *req){ return do_lua(req,"home");