diff --git a/assets/faq.html b/assets/faq.html new file mode 100644 index 0000000..69723e9 --- /dev/null +++ b/assets/faq.html @@ -0,0 +1,86 @@ + + + + + 🍑 + + + + +
+

FAQ

+ An attempt to answer frequently asked questions, and document + the detail of site features. As with all software documentation, + this is how the developer thinks these things work, + to find out how things actually work, see the source code. +

How do a register an account?

+ You can go here to register an account, + although you can also post pastes and comments anonymously. +

Parsers? What are those?

+ Parsers are the way you can style the text you post. While + this software started as a pastebin clone, the need to style + text better than pastebin.com quickly became apparent. Two + parsers are currently available: +

The plain parser

+

The goal of the plain parser is to format plain text exactly as you would expect it. It does not try to to anything fancy and tries to be faithful to the plaintext representation. +

Imageboard parser

+

The imageboard parser delivers an approximation of Infinity markup, and will be familiar to those who have used imageboards before. The imageboard parser supports the following markup: +

Surround text with double single-quotes(') to make text italic +

Surround text with triple single-quotes to make text bold +

Surround text with underscores(_) to make it underlined +

Surround text with double asterisks(*) to make it spoilered +

Surround text with tildes(~) to make it strike through +

Begin a line with a greater-than followed by a space to make it +

>greentext +

Begin a line with a less-than followed by a space to make it +

<pinktext +

Surround text with forum-style [spoiler] and [/spoiler] tags as a second way to spoiler +

Surround text with forum-style [code] and [/code] tags to make it + +


+Preformatted and monospace
+			
+ +

If you have incomplete markup at the end, it shouldn't break anything, let me know if it does. +

How does search work?

+

The search utility searches for stories on the site. + At it's most simple, it searches stories based on tags, + but it can also filter stories based on: title, author, + date, and hits. In general, the syntax for search is + +|-<field>>operator<>value< +

The first + or - specifies + weather to include or exclude results based on this + search, the <field> specifies what + field to search for (or search based on tag if this is + missing), and <operator> specifies + how to search. +

For title, and author, the only allowed operator is + =. This operator will search for the text + appearing anywhere in the field, case insensitive. For + hits and time, the allowed operators are + >,<,>=, + <=,=, which searches for + greater than, less than, greater than or equal to, less + than or equal to, and strictly equal to, respectively. + Tag does not need a field or operator, and only allows + exact matches. As a quirk of this system, it is + impossible to search for the tags "author", "title", + "hits" or "date", + + Examples: + +

+author=admin -meta
+ Will return all stories by the users "admin" and "badminton_enthusiast" that do not include the "meta" tag. +
+hits>20 -date<=1609459201
+ Will return all stories with more than 20 hits that were posted before January 1, 2021 (unix timestamp 1609459201).
+ While the date field is a little hard to use for humans, it may be useful for robots. +

How do I enable reader mode on my story?

+ Unfortunately, there is no web standard about how reader + modes for different browsers work, and no way to hint to + the browser that a page is readable. The site makes a + best effort to trigger browsers into thinking a post is + readable, but this is unreliable. In general, if reader mode + is broken you just need to make your post a little longer. +
+ + diff --git a/assets/style.css b/assets/style.css index 1c42170..78c1974 100644 --- a/assets/style.css +++ b/assets/style.css @@ -20,7 +20,19 @@ p,.tag-list{margin-bottom:0px} padding: 0 1em 0 1em; margin: 0 1px 0 1px; } +.search{ + display:flex !important; + flex:0.5 0.5 auto !important; +} +.search>.button{ + flex:10 10 auto; + translate: -100%; +} +.column-0{margin-right:5px;} @media (prefers-color-scheme: dark){ - @import "css/style_dark.css"; + body, form>*{ + background: #1c1428; + color: #d0d4d8 !important; + } } diff --git a/conf/smr.conf b/conf/smr.conf index 35262be..9c18464 100644 --- a/conf/smr.conf +++ b/conf/smr.conf @@ -36,6 +36,7 @@ domain * { route /_css/milligram.css asset_serve_milligram_css route /_css/milligram.min.css.map asset_serve_milligram_min_css_map route /_css/style_dark.css asset_serve_style_dark_css + route /_faq asset_serve_faq_html route /favicon.ico asset_serve_favicon_ico route /_paste post_story route /_edit edit_story @@ -77,7 +78,7 @@ domain * { validate tags v_any } params get /_search { - validate tag v_any + validate q v_any } params get ^/[^_].* { validate comments v_bool diff --git a/src/lua/endpoints/download_get.lua b/src/lua/endpoints/download_get.lua index 7c17085..920c727 100644 --- a/src/lua/endpoints/download_get.lua +++ b/src/lua/endpoints/download_get.lua @@ -25,7 +25,7 @@ local function download_get(req) local err = util.do_sql(stmnt_download) if err == sql.DONE then --No rows, story not found - http_responose(req,404,pages.nostory{path=story}) + http_response(req,404,pages.nostory{path=story}) stmnt_download:reset() return end diff --git a/src/lua/endpoints/index_get.lua b/src/lua/endpoints/index_get.lua index 104d09b..c9ef89e 100644 --- a/src/lua/endpoints/index_get.lua +++ b/src/lua/endpoints/index_get.lua @@ -52,21 +52,17 @@ local function get_author_home(req) stmnt_author_bio:bind_names{author=subdomain} local err = util.do_sql(stmnt_author_bio) if err == sql.DONE then - print("No such author") stmnt_author_bio:reset() return pages.noauthor{ author = subdomain } end - print("err:",err) assert(err == sql.ROW,"failed to get author:" .. subdomain .. " error:" .. tostring(err)) local data = stmnt_author_bio:get_values() local bio = data[1] stmnt_author_bio:reset() - print("Getting author's stories") stmnt_author:bind_names{author=subdomain} err = util.do_sql(stmnt_author) - print("err:",err) local stories = {} while err == sql.ROW do local data = stmnt_author:get_values() diff --git a/src/lua/endpoints/search_get.lua b/src/lua/endpoints/search_get.lua index 4c340a3..d7010c3 100644 --- a/src/lua/endpoints/search_get.lua +++ b/src/lua/endpoints/search_get.lua @@ -6,6 +6,7 @@ local util = require("util") local libtags = require("tags") local pages = require("pages") local config = require("config") +local search_parser = require("parser_search") local stmnt_search local oldconfigure = configure @@ -18,42 +19,40 @@ local function search_get(req) local host = http_request_get_host(req) local path = http_request_get_path(req) http_request_populate_qs(req) - local tag = http_argument_get_string(req,"tag") - if tag then - stmnt_search:bind_names{ - tag = tag - } - local results = {} - local err - repeat - err = stmnt_search:step() - if err == sql.BUSY then - coroutine.yield() - elseif err == sql.ROW then - local id, title, anon, time, author = unpack(stmnt_search:get_values()) - local idp = util.encode_id(id) - local tags = libtags.get(id) - table.insert(results,{ - id = idp, - title = title, - anon = anon, - time = os.date("%B %d %Y",tonumber(time)), - author = author, - tags = tags - }) - elseif err == sql.DONE then - stmnt_search:reset() - else - error("Failed to search, sql error:" .. tostring(err)) + local searchq = assert(http_argument_get_string(req,"q")) + log(LOG_DEBUG,string.format("search: %q",searchq)) + local sqltxt, data = search_parser(searchq) + local stmnt = assert(db.conn:prepare(sqltxt), db.conn:errmsg()) + local i = 1 + for field,values in pairs(data) do + if field ~= "tags" then + for _, value in pairs(values) do + stmnt:bind(i,value[3]) + i = i + 1 end - until err == sql.DONE - local ret = pages.search{ - domain = config.domain, - results = results, - tag = tag, - } - http_response(req,200,ret) + end end + for _,value in pairs(data.tags) do + stmnt:bind(i,value[3]) + i = i + 1 + end + local results = {} + for row in stmnt:rows() do + table.insert(results,{ + id = util.encode_id(row[1]), + title = row[2], + isanon = row[3] == 1, + author = row[4], + time = os.date("%B %d %Y",tonumber(row[5])), + tags = libtags.get(row[1]) + }) + end + local ret = pages.search{ + domain = config.domain, + results = results, + q = searchq, + } + http_response(req,200,ret) end return search_get diff --git a/src/lua/parser_search.lua b/src/lua/parser_search.lua index 69a26a3..86ed68b 100644 --- a/src/lua/parser_search.lua +++ b/src/lua/parser_search.lua @@ -1,10 +1,84 @@ +local lpeg = require('lpeg') +local etlua = require('etlua') +local args = {...} +lpeg.locale(lpeg) +local V,P,C,S,B,Cs = lpeg.V,lpeg.P,lpeg.C,lpeg.S,lpeg.B,lpeg.Cs +--Identity function +local ident = function(a) return a end +--Lowercase and capitalize first letter +local function capitalize(str) + return string.lower(str):gsub("^(.)",string.upper) +end +--Trim whitespace +local function trim(str) + return str:match("^%s*(.-)%s*$") +end +--SQL like match anywhere +local function like(str) + return "%" .. str .. "%" +end +--Tags are always trimed of whitepsace, lowercase with first letter capitalized +local function tag_fmt(str) + return capitalize(trim(str)) +end +--Title match +local function title_fmt(str) + return like(trim(str)) +end +--Author names are all lowercase alphanumeric, max 30 characters +local function author_fmt(str) + return like(string.lower(trim(str))) +end +local fieldnames = { + title = {name="title",type="string",fmt=title_fmt}, + author = {name="author",type="string",fmt=author_fmt}, + date = {name="date",type="number",fmt=tonumber}, + hits = {name="hits",type="number",fmt=tonumber}, + tags = {name="tag",type="string",fmt=tag_fmt}, +} +local field_default = "tag" +local fields +local grammar = P{ + "chunk"; + whitespace = S" \t\n"^0, + itm = C(P(1-S"+-")^0), --go until the next '+' or '-' + likefield = C(P"title" + P"author") * V"whitespace" * C(P"=") * V"whitespace" * V"itm", + rangeop = P"<=" + P">=" + P">" + P"<" + P"=", + rangefield = C(P"date" + P"hits") * V"whitespace" * C(V"rangeop") * V"whitespace" * C(V"itm"), + field = C(S"+-") * (V"likefield" + V"rangefield" + V"itm") / function(pn,field,expr,value) + if expr and value then + fields[field] = fields[field] or {} + table.insert(fields[field],{pn,expr,value}) + else + fields.tags = fields.tags or {} + table.insert(fields.tags,{pn,"=",field}) + end + end, + chunk = V"field"^0 +} --Grammar --Transpile a sting with + and - into an sql query that searches tags +local fname = "pages/search_sql.etlua" +local sqltmpl = assert(io.open(fname)) +local c = etlua.compile(sqltmpl:read("*a"),fname) +sqltmpl:close() local function transpile(str) - for chunk in str:gmatch("([+-])([^+-])") do - print("found chunk:",chunk) + str = string.lower(str) + fields = {tags={}} + table.concat({grammar:match(str)}," ") + --Sanity perform formatting on data + for field,values in pairs(fields) do + for _,value in pairs(values) do + local pn, expr, val = unpack(value) + local nval = fieldnames[field].fmt(val) + value[3] = nval + end end + local ressql = c{ + result = fields + } + return ressql, fields end return transpile diff --git a/src/lua/search_sql.etlua b/src/lua/search_sql.etlua new file mode 100644 index 0000000..333889d --- /dev/null +++ b/src/lua/search_sql.etlua @@ -0,0 +1,44 @@ +SELECT + posts.id, + posts.post_title, + authors.name, + posts.post_time +FROM + posts,authors +WHERE + authors.id = posts.authorid +<% print("reqult:",result) -%> +<% for field, values in pairs(result) do -%> + <% print("values:",values) -%> + <% for _,value in pairs(values) do -%> + <% local pn,expr,value = unpack(value) -%> + <% local n = (pn == "+" and "" or "NOT") -%> + <% if field == "title" then -%> + AND <%= n %> posts.post_title LIKE ? + <% elseif field == "author" then -%> + AND <%= n %> authors.name LIKE ? + <% elseif field == "date" then -%> + AND <%= n %> posts.post_time <%- expr %> ? + <% elseif field == "hits" then -%> + AND posts.views <%- expr -%> ? + <% end -%> + <% end -%> +<% end -%> +<% print("tags:",result.tags) %> +<% for _,tag in pairs(result.tags) do -%> +INTERSECT +SELECT + posts.id, + posts.post_title, + authors.name, + posts.post_time +FROM + posts,authors,tags +WHERE + posts.authorid = authors.id + AND tags.postid = posts.id + <% local n,v = unpack(tag) -%> + <% n = (n == "-" and "NOT" or "") -%> + AND <%= n %> tags.tag = <%= v %> +<% end -%> +; diff --git a/src/lua/util.lua b/src/lua/util.lua index 32da51f..f844a63 100644 --- a/src/lua/util.lua +++ b/src/lua/util.lua @@ -45,7 +45,7 @@ function util.sqlbind(stmnt,call,position,data) assert(call == "bind" or call == "bind_blob","Bad bind call, call was:" .. call) local f = stmnt[call](stmnt,position,data) if f ~= sql.OK then - error(string.format("Failed to %s at %d with %q: %s", call, position, data, db:errmsg()),2) + error(string.format("Failed to %s at %d with %q: %s", call, position, data, db.conn:errmsg()),2) end end diff --git a/src/pages/index.etlua b/src/pages/index.etlua index 7a6bf33..012dc51 100644 --- a/src/pages/index.etlua +++ b/src/pages/index.etlua @@ -15,9 +15,15 @@
- New paste - Log in - Register +
+ New paste + Log in + Register + +

Welcome to slash.monster, stories of fiction and fantasy
Not safe for work
@@ -43,7 +49,7 @@

<%= v.time %> diff --git a/src/pages/search_sql.etlua b/src/pages/search_sql.etlua new file mode 100644 index 0000000..1b447dd --- /dev/null +++ b/src/pages/search_sql.etlua @@ -0,0 +1,43 @@ +SELECT + posts.id, + posts.post_title, + posts.isanon, + authors.name, + posts.post_time +FROM + posts,authors +WHERE + authors.id = posts.authorid +<% for field, values in pairs(result) do -%> + <% for _,value in pairs(values) do -%> + <% local pn,expr,value = unpack(value) -%> + <% local n = (pn == "+" and "" or "NOT") -%> + <% if field == "title" then -%> + AND <%= n %> posts.post_title LIKE ? + <% elseif field == "author" then -%> + AND <%= n %> authors.name LIKE ? + <% elseif field == "date" then -%> + AND <%= n %> posts.post_time <%- expr %> ? + <% elseif field == "hits" then -%> + AND posts.views <%- expr -%> ? + <% end -%> + <% end -%> +<% end -%> +<% for _,tag in pairs(result.tags) do -%> +INTERSECT +SELECT + posts.id, + posts.post_title, + posts.isanon, + authors.name, + posts.post_time +FROM + posts,authors,tags +WHERE + posts.authorid = authors.id + AND tags.postid = posts.id + <% local n,v,t = unpack(tag) -%> + <% n = (n == "-" and "NOT" or "") -%> + AND <%= n %> tags.tag = ? +<% end -%> +;