Start work on tag suggestions

Start working on getting tag suggestions for pasteing and editing pages.
This commit is contained in:
Robin Malley 2021-02-14 07:30:20 +00:00
parent 67de40c02b
commit 896f452fa6
10 changed files with 207 additions and 4 deletions

105
assets/suggest_tags.js Normal file
View File

@ -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 <input/>
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);

View File

@ -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
}
}

View File

@ -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

View File

@ -61,7 +61,10 @@ local function edit_get(req)
story = story_id,
err = "",
tags = tags_txt,
unlisted = unlisted == 1
unlisted = unlisted == 1,
extra_load = {
'<script src="/_js/suggest_tags.js"></script>'
}
}
http_response(req,200,ret)
end

View File

@ -19,6 +19,9 @@ local function paste_get(req)
return assert(pages.paste{
domain = config.domain,
err = "",
extra_load = {
'<script src="/_js/suggest_tags.js"></script>'
}
})
end)
http_response(req,200,text)
@ -29,6 +32,9 @@ local function paste_get(req)
user = author,
err = "",
text="",
extra_load = {
'<script src="/_js/suggest_tags.js"></script>'
}
})
elseif host ~= config.domain and author == nil then
http_response_header(req,"Location",string.format("https://%s/_paste",config.domain))

View File

@ -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")

View File

@ -41,7 +41,7 @@
<textarea name="text" cols=80 rows=24 class="column"><%= text %></textarea><br/>
</div>
<input type="submit">
<input type="submit" formtarget="_blank" value="Preview" formaction="https://<%= domain %>/_preview">
</fieldset>
</form>
<{cat src/pages/parts/footer.etlua}>

View File

@ -14,6 +14,11 @@
<% end %>
<link href="/_css/milligram.css" rel="stylesheet">
<link href="/_css/style.css" rel="stylesheet">
<% if extra_load then %>
<% for _,load in ipairs(extra_load) do %>
<%- load %>
<% end %>
<% end %>
</head>
<body class="container">
<main class="wrapper">

View File

@ -1,4 +1,4 @@
<{system cat src/pages/parts/header.etlua}>
<{system cat src/pages/parts/header.etlua}>
<h1 class="title">
Paste
</h1>

View File

@ -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");