Merge branch 'new_async'

Conflicts:
	forum.nim
	main.tmpl
This commit is contained in:
Dominik Picheta 2015-02-14 14:37:47 +00:00
commit 6da5f369db
39 changed files with 1769 additions and 1116 deletions

1
.gitignore vendored
View file

@ -1,6 +1,7 @@
# Wildcard patterns. # Wildcard patterns.
*.swp *.swp
nimcache/ nimcache/
*.db
# Specific paths # Specific paths
/createdb /createdb

32
cache.nim Normal file
View file

@ -0,0 +1,32 @@
import tables, uri
type
CacheInfo = object
valid: bool
value: string
CacheHolder = ref object
caches: Table[string, CacheInfo]
proc normalizePath(x: string): string =
let u = parseUri(x)
result = u.path & (if u.query != "": '?' & u.query else: "")
proc newCacheHolder*(): CacheHolder =
new result
result.caches = initTable[string, CacheInfo]()
proc invalidate*(cache: CacheHolder, name: string) =
cache.caches.mget(name.normalizePath()).valid = false
proc invalidateAll*(cache: CacheHolder) =
for key, val in mpairs(cache.caches):
val.valid = false
template get*(cache: CacheHolder, name: string, grabValue: expr): expr =
## Check to see if the cache contains value for ``name``. If it does and the
## cache is valid then doesn't recalculate it but returns the cached version.
mixin normalizePath
let nName = name.normalizePath()
if not (cache.caches.hasKey(nName) and cache.caches[nName].valid):
cache.caches[nName] = CacheInfo(valid: true, value: grabValue)
cache.caches[nName].value

View file

@ -11,7 +11,7 @@ import cairo, os, strutils, jester
proc getCaptchaFilename*(i: int): string {.inline.} = proc getCaptchaFilename*(i: int): string {.inline.} =
result = "public/captchas/capture_" & $i & ".png" result = "public/captchas/capture_" & $i & ".png"
proc getCaptchaUrl*(req: var TRequest, i: int): string = proc getCaptchaUrl*(req: PRequest, i: int): string =
result = req.makeUri("/captchas/capture_" & $i & ".png", absolute = false) result = req.makeUri("/captchas/capture_" & $i & ".png", absolute = false)
proc createCaptcha*(file, text: string) = proc createCaptcha*(file, text: string) =

View file

@ -8,7 +8,7 @@
import strutils, db_sqlite import strutils, db_sqlite
var db = Open(connection="nimforum.db", user="postgres", password="", var db = open(connection="nimforum.db", user="postgres", password="",
database="nimforum") database="nimforum")
const const
@ -16,7 +16,7 @@ const
TPassword = "varchar(32)" TPassword = "varchar(32)"
TEmail = "varchar(30)" TEmail = "varchar(30)"
db.Exec(sql""" db.exec(sql"""
create table if not exists thread( create table if not exists thread(
id integer primary key, id integer primary key,
name varchar(100) not null, name varchar(100) not null,
@ -24,11 +24,11 @@ create table if not exists thread(
modified timestamp not null default (DATETIME('now')) modified timestamp not null default (DATETIME('now'))
);""", []) );""", [])
db.Exec(sql""" db.exec(sql"""
create unique index if not exists ThreadNameIx on thread (name); create unique index if not exists ThreadNameIx on thread (name);
""", []) """, [])
db.Exec(sql(""" db.exec(sql("""
create table if not exists person( create table if not exists person(
id integer primary key, id integer primary key,
name $# not null, name $# not null,
@ -42,14 +42,19 @@ create table if not exists person(
);""" % [TUserName, TPassword, TEmail]), []) );""" % [TUserName, TPassword, TEmail]), [])
# echo "person table already exists" # echo "person table already exists"
db.Exec(sql""" db.exec(sql("""
alter table person
add ban varchar(128) not null default ''
"""))
db.exec(sql"""
create unique index if not exists UserNameIx on person (name); create unique index if not exists UserNameIx on person (name);
""", []) """, [])
# ----------------------- Forum ------------------------------------------------ # ----------------------- Forum ------------------------------------------------
if not db.TryExec(sql""" if not db.tryExec(sql"""
create table if not exists post( create table if not exists post(
id integer primary key, id integer primary key,
author integer not null, author integer not null,
@ -66,7 +71,7 @@ create table if not exists post(
# -------------------- Session ------------------------------------------------- # -------------------- Session -------------------------------------------------
if not db.TryExec(sql(""" if not db.tryExec(sql("""
create table if not exists session( create table if not exists session(
id integer primary key, id integer primary key,
ip inet not null, ip inet not null,
@ -77,7 +82,7 @@ create table if not exists session(
);""" % [TPassword]), []): );""" % [TPassword]), []):
echo "session table already exists" echo "session table already exists"
if not db.TryExec(sql""" if not db.tryExec(sql"""
create table if not exists antibot( create table if not exists antibot(
id integer primary key, id integer primary key,
ip inet not null, ip inet not null,
@ -86,8 +91,35 @@ create table if not exists antibot(
);""", []): );""", []):
echo "antibot table already exists" echo "antibot table already exists"
# -------------------- Search --------------------------------------------------
if not db.tryExec(sql"""
CREATE VIRTUAL TABLE thread_fts USING fts4 (
id INTEGER PRIMARY KEY,
name VARCHAR(100) NOT NULL
);""", []):
echo "thread_fts table already exists or fts4 not supported"
else:
db.exec(sql"""
INSERT INTO thread_fts
SELECT id, name FROM thread;
""", [])
if not db.tryExec(sql"""
CREATE VIRTUAL TABLE post_fts USING fts4 (
id INTEGER PRIMARY KEY,
header VARCHAR(100) NOT NULL,
content VARCHAR(1000) NOT NULL
);""", []):
echo "post_fts table already exists or fts4 not supported"
else:
db.exec(sql"""
INSERT INTO post_fts
SELECT id, header, content FROM post;
""", [])
# ------------------------------------------------------------------------------
#discard stdin.readline() #discard stdin.readline()
Close(db) close(db)

View file

@ -13,43 +13,74 @@
# #
# result = "" # result = ""
# count = 0 # count = 0
<table id="threads"> <div id="talk-heads">
<tr> <div class="topic">
<th>Topics</th> <div>
<th>Author</th> Topic
<th>Posts</th> <a href="${c.req.makeUri("/threadActivity.xml")}">
<th>Views</th> <img src="/images/Feed-icon.svg" class="rssfeed">
<th>Last reply</th> </a>
</tr> </div>
# for row in Rows(db, query, $((c.pageNum-1) * ThreadsPerPage), $ThreadsPerPage): </div>
<div class="users"><div>Users</div></div>
<div class="detail"><div>Details</div></div>
<div class="activity">
<div>
Activity
<a href="${c.req.makeUri("/postActivity.xml")}">
<img src="/images/Feed-icon.svg" class="rssfeed">
</a>
</div>
</div>
</div>
<div id="talk-threads">
# for row in rows(db, query, $((c.pageNum-1) * ThreadsPerPage), $ThreadsPerPage):
# inc(count) # inc(count)
<tr> <div>
<td class="topic"> <div class="topic">
${UrlButton(c, XMLencode(%name), c.genThreadUrl(threadid = %threadid))} <div>
${genPagenumLocalNav(c, (%threadid).parseInt)} <a href="${c.genThreadUrl(threadid = %threadid)}"
</td> title="${xmlEncode(%name)}">${xmlEncode(%name)}</a>
#let authorName = getValue(db, sql("select name from person where id = " & ${genPagenumLocalNav(c, (%threadid).parseInt)}
# "(select author from post where id = " & </div>
# "(select min(id) from post where thread = ?))"), %threadId) </div>
#let profileUrl = c.req.makeUri("profile/", false) & XMLEncode(authorName)
<td class="author"><a href="$profileUrl">${authorName}</a></td> #let users = getAllRows(db,
# let posts = GetValue(db, sql"select count(*) from post where thread = ?", %threadId) # sql("select distinct name, email from person where id in " &
<td class="posts">$posts</td> # "(select author from post where thread = ?)"), %threadId)
<td class="views">${XMLencode(%views)}</td> <div class="users">
<div>
#for i in 0 .. min(6, users.len-1):
<img src="${getGravatarUrl(users[i][1], 20)}" title="${users[i][0]}">
#end for
</div>
</div>
#let latestReplyAuthor = getValue(db, sql("select name from person where id = " & #let latestReplyAuthor = getValue(db, sql("select name from person where id = " &
# "(select author from post where id = " & # "(select author from post where id = " &
# "(select max(id) from post where thread = ?))"), %threadId) # "(select max(id) from post where thread = ?))"), %threadId)
#let replyProfileUrl = c.req.makeUri("profile/", false) &
# xmlEncode(latestReplyAuthor)
# let posts = getValue(db, sql"select count(*) from post where thread = ?", %threadId)
<div class="detail">
<div><div title="Views">${xmlEncode(%views)}</div></div>
<div><div title="Posts">$posts</div></div>
</div>
#let latestReplyDate = getValue(db, sql("SELECT strftime('%s', " & #let latestReplyDate = getValue(db, sql("SELECT strftime('%s', " &
# "(select creation from post where id = (select max(id) from post where thread = ?)))"), %threadId) # "(select creation from post where id = (select max(id) from post where thread = ?)))"), %threadId)
<td class="lastreply"> #let timeStr = formatTimestamp(latestReplyDate.parseInt())
<span>${formatTimestamp(latestReplyDate.parseInt())}</span><br/> <div class="activity">
#let replyProfileUrl = c.req.makeUri("profile/", false) & <div>
# XMLEncode(latestReplyAuthor) <a href="$replyProfileUrl">$latestReplyAuthor</a> replied $timeStr
<span><a href="$replyProfileUrl">${latestReplyAuthor}</a></span> </div>
</td> </div>
</tr> </div>
# end for # end for
</table> </div>
#end proc #end proc
# #
# #
@ -57,26 +88,26 @@
# title, content, author, date: string): string = # title, content, author, date: string): string =
# result = "" # result = ""
<a name="preview"></a> <a name="preview"></a>
<table class="post"> <div id="talk-thread">
<tr> <div>
<th colspan="2"> <div class="author">
<span>${XMLEncode(title)}</span> <div>
<span style="float:right;">${XMLencode(date)}</span> #let profileUrl = c.req.makeUri("profile/", false) & xmlEncode(author)
</th> <a class="name" href="$profileUrl">${xmlEncode(author)}</a>
</tr> </div>
<tr> </div>
<td class="left"> <div class="topic">
<span>${XMLencode(author)}</span> <div>
</td> #try:
<td class="content"> ${content.rstToHtml}
#try: #except EParseError:
${content.rstToHtml} # c.errorMsg = getCurrentExceptionMsg()
#except EParseError: #end
# c.errorMsg = getCurrentExceptionMsg() <span class="date">${xmlEncode(date)}</span>
#end </div>
</td> </div>
</tr> </div>
</table> </div>
#end proc #end proc
# #
# #
@ -92,39 +123,47 @@
# const userEmail = 6 # const userEmail = 6
# result = "" # result = ""
# count = 0 # count = 0
# for row in FastRows(db, query, threadId, $((c.pageNum-1) * PostsPerPage), $PostsPerPage): # let posts = getAllRows(db, query, threadId, $((c.pageNum-1) * PostsPerPage), $PostsPerPage)
# if posts.len < 1: return ""
# end if
<div id="talk-head">
<div class="info-post">
<div>
<a href="${c.req.makeUri("/")}"><b>forum index</b></a> &gt;
<a href="${c.req.makeUri("/t/" & $threadId)}">${posts[0][postHeader]}</a>
</div>
</div>
</div>
<div id="talk-thread">
# for row in posts:
# inc(count) # inc(count)
<a name="${%postId}"></a> <a name="${%postId}"></a>
<table class="post"> <div id="${%postId}">
<tr> <div class="author">
<th colspan="2"> <div>
<span>${XMLencode(%postHeader)}</span> #let profileUrl = c.req.makeUri("profile/", false) & xmlEncode(%userName)
<span style="float:right;">${XMLencode(%postCreation)}</span> <div class="avatar">${genGravatar(%userEmail)}</div>
</th> <a class="name" href="$profileUrl">${xmlEncode(%userName)}</a>
</tr> #if c.userId == %postAuthor and c.currentPost.subject.len == 0:
<tr> <hr/><a href="${c.genThreadUrl(%postId, "edit")}">Edit post</a>
<td class="left"> #elif c.isAdmin and c.currentPost.subject.len == 0:
#let profileUrl = c.req.makeUri("profile/", false) & XMLencode(%userName) <hr/><a style="color: red;" href="${c.genThreadUrl(%postId, "edit")}">Edit post</a>
<span><a href="$profileUrl">${XMLencode(%userName)}</a></span> #end if
<hr/> </div>
<a href="$profileUrl">${genGravatar(%userEmail)}</a> </div>
#if c.userId == %postAuthor and c.currentPost.subject.len == 0: <div class="topic">
<hr/>${UrlButton(c, "Edit post", c.genThreadUrl(%postId, "edit"))} <div>
#elif c.isAdmin and c.currentPost.subject.len == 0: #try:
<hr/><span style="color:red"> ${(%postContent).rstToHtml}
${UrlButton(c, "Edit post", c.genThreadUrl(%postId, "edit"))}</span> #except EParseError:
#end if # c.errorMsg = getCurrentExceptionMsg()
</td> #end
<td class="content"> <span class="date">${xmlEncode(%postCreation)}</span>
#try: </div>
${(%postContent).rstToHtml} </div>
#except EParseError: </div>
# c.errorMsg = getCurrentExceptionMsg()
#end
</td>
</tr>
</table>
# end for # end for
</div>
#end proc #end proc
# #
# #
@ -134,25 +173,39 @@
<br /> <br />
<a name="reply"></a> <a name="reply"></a>
<div id="replywrapper"> <div id="replywrapper">
<div id="replytop"> <div id="talk-head">
<span>${topText}</span> <div class="info-post">
<div>
<a href="${c.req.makeUri("/")}"><b>forum index</b></a> &gt;
$topText
</div>
</div>
</div> </div>
<form action="${c.req.makeUri(action, false) & "#preview"}" method="POST"> <form action="${c.req.makeUri(action, false) & "#preview"}" method="POST">
${FieldValid(c, "subject", "Subject:")} #if action == "doreply":
${TextWidget(c, "subject", title, maxlength=100)} ${HiddenField(c, "subject", title)}
<br /> #else:
${FieldValid(c, "subject", "Subject:")}
${TextWidget(c, "subject", title, maxlength=100)}
<br />
#end if
${FieldValid(c, "content", "Content:")}<br /> ${FieldValid(c, "content", "Content:")}<br />
${TextAreaWidget(c, "content", content, width=100, height=20)}<br /> ${TextAreaWidget(c, "content", content)}<br />
${FormSession(c, action)} ${FormSession(c, action)}
# if isEdit: # if isEdit:
<input type="checkbox" name="delete" value="Delete">Delete Post<br /> <input type="checkbox" name="delete" value="Delete">Delete Post<br />
# end if # end if
#if c.errorMsg != "":
<div style="float: left; width: 100%;">
<span class="error">$c.errorMsg</span>
</div>
#end if
<br/> <br/>
<input type="submit" name="previewBtn" value="Preview" /> <input type="submit" name="previewBtn" value="Preview" />
<input type="submit" name="postBtn" value="Submit" /> <input type="submit" name="postBtn" value="Submit" />
<a href="http://nimrod-lang.org/rst.html">Syntax Cheatsheet</a> <a href="http://nim-lang.org/rst.html">Syntax Cheatsheet</a>
</form> </form>
</div> </div>
#end proc #end proc
@ -160,8 +213,15 @@
# #
#proc genFormRegister(c: var TForumData): string = #proc genFormRegister(c: var TForumData): string =
# result = "" # result = ""
<div id="talk-head">
<div class="info-post">
<div>
<a href="${c.req.makeUri("/")}"><b>forum index</b></a> &gt;
Register
</div>
</div>
</div>
<form action="${c.req.makeUri("/doregister", false)}" method="POST"> <form action="${c.req.makeUri("/doregister", false)}" method="POST">
<b>Register</b><br />
<table border="0"> <table border="0">
<tr> <tr>
<td>${FieldValid(c, "name", "Username:")}</td> <td>${FieldValid(c, "name", "Username:")}</td>
@ -180,6 +240,11 @@
<td>${TextWidget(c, "antibot", "", maxlength=4)}</td> <td>${TextWidget(c, "antibot", "", maxlength=4)}</td>
</tr> </tr>
</table> </table>
#if c.errorMsg != "":
<div style="float: left; width: 100%;">
<span class="error">$c.errorMsg</span>
</div>
#end if
<input type="submit" value="Register"> <input type="submit" value="Register">
</form> </form>
#end proc #end proc
@ -205,38 +270,100 @@
# #
#proc genListOnline(c: var TForumData, stats: TForumStats): string = #proc genListOnline(c: var TForumData, stats: TForumStats): string =
# result = "" # result = ""
<div id="whoisonline"> # var active: seq[string] = @[]
<div class="wioHeader"> # for i in stats.activeUsers:
<span>Who is online?<span> # active.add(i.nick)
</div> # end for
<div class="content"> # let profileUrl = c.req.makeUri("profile/", false) &
<span>Out of ${stats.totalUsers} users ${stats.activeUsers.len} are online${if stats.activeUsers.len == 0: "." else: ":"} # xmlEncode(stats.newestMember.nick)
#for index, usr in stats.activeUsers: <span class="forum-user-info" title="${active.join(", ")}">
# let profileHref = """<a href="""" & c.req.makeUri("profile/", false) & <b>${stats.activeUsers.len}</b> of <b>${stats.totalUsers}</b> users online</span> &nbsp;|&nbsp;
# XMLencode(usr.nick) & """">""" <b>${stats.totalThreads}</b> threads &nbsp;|&nbsp; <b>${stats.totalPosts}</b> posts &nbsp;|&nbsp;
# let hrefEnd = """</a>""" newest member: <a href="$profileUrl">${stats.newestMember.nick}</a>
# if usr.isAdmin: #end proc
#if index != 0: result.add ',' #
#end if #
#result.add("""<span class="user admin"> """ & profileHref & #
# usr.nick & hrefEnd & """</span>""") #
# else: #proc genSearchResults(c: var TForumData,
#if index != 0: result.add ',' # results: iterator: db_sqlite.TRow {.closure, tags: [FReadDB].},
#end if # count: var int): string =
#result.add("""<span class="user"> """ & profileHref & # const threadId = 0
# usr.nick & hrefEnd & """</span>""") # const threadName = 1
# end if # const postId = 2
#end for # const postHeader = 3
</span> # const postContent = 4
<hr/> # const userName = 5
#if stats.newestMember.nick != "": # const postCreation = 6
#let profileUrl = c.req.makeUri("profile/", false) & # const postAuthor = 7
# XMLEncode(stats.newestMember.nick) # const userEmail = 8
<span>Total threads: ${stats.totalThreads} | Total posts: ${stats.totalPosts} | Newest member: <a href="$profileUrl">${stats.newestMember.nick}</a></span> # const what = 9
#else: # result = ""
<span>Total threads: ${stats.totalThreads} | Total posts: ${stats.totalPosts}</span> # count = 0
#end if # var whCount: array[bool, int]
<div id="talk-head">
<div class="info-post">
<div>
Search results for: <i style="color: #332299">${xmlEncode(c.search.replace("&quot;","\""))}</i>.
</div> </div>
</div> </div>
</div>
<div id="talk-thread" class="searchResults">
# for row in results():
# inc(count)
# let isThread = %what == "0"
# inc(whCount[isThread])
# let postUrl = c.genThreadUrl(%postId,"",%threadId,"")
# let threadUrl = c.genThreadUrl("","",%threadId)
# var headersDiffer = false
<div>
<div class="author">
<div>
#let profileUrl = c.req.makeUri("profile/", false) & xmlEncode(%userName)
<div><a href="$profileUrl">${genGravatar(%userEmail, 40)}</a></div>
<div style="padding: 8px 0"><a href="$profileUrl">${xmlEncode(%userName)}</a></div>
#if c.userId == %postAuthor and c.currentPost.subject.len == 0:
<hr/><a href="${c.genThreadUrl(%postId, "edit", %threadId)}">Edit post</a>
#elif c.isAdmin and c.currentPost.subject.len == 0:
<hr/><a style="color: red;" href="${c.genThreadUrl(%postId, "edit", %threadId)}">Edit post</a>
#end if
</div>
</div>
<div class="topic">
<div>
#if %postHeader != "":
<div class="postTitle">
<span class="titleHeader">Post:</span>
<a href="${postUrl}">
<span>${%postHeader}</span>
</a>
</div>
#end if
#if not isThread:
#try:
${(%postContent).rstToHtml}
#except EParseError:
# c.errorMsg = getCurrentExceptionMsg()
${xmlEncode(%postContent)}
#end
#end if
<span class="date">${xmlEncode(%postCreation)}</span>
</div>
</div>
</div>
# end for
</div>
# if c.pageNum > 1:
<form action="/search/${$(c.pageNum-1)}" method="post" class="searchNav">
<input type="hidden" name="q" value="${c.search}">
<input type="submit" value="Previous ${ThreadsPerPage} results">
</form>
# end if
# if whCount[true] == ThreadsPerPage or whCount[false] == ThreadsPerPage:
<form action="/search/${$(c.pageNum+1)}" method="post" class="searchNav">
<input type="hidden" name="q" value="${c.search}">
<input type="submit" value="Next ${ThreadsPerPage} results (if any)">
</form>
# end if
#end proc #end proc
#

739
forum.nim

File diff suppressed because it is too large Load diff

View file

@ -3,4 +3,3 @@
--path:"$nimrod/lib/packages/docutils" --path:"$nimrod/lib/packages/docutils"
--path:"$nimrod" --path:"$nimrod"
--path:"../jester"

74
fts.sql Normal file
View file

@ -0,0 +1,74 @@
-- selects just threads,
-- those where title doesn't coinside with some of its posts' titles
-- by now selects only the threads title (no post snippet)
SELECT
thread_id,
snippet(thread_fts, '<b>', '</b>', '<b>...</b>') AS thread,
0 AS post_id,
'' AS header,
'' AS content,
person.name AS author,
cdate,
author_id,
person.email AS email,
0 AS what
FROM (
SELECT
thread_fts.id AS thread_id,
post.id AS post_id,
post.creation AS cdate,
MIN(post.creation) AS cdate,
post.author AS author_id
FROM thread_fts
JOIN post ON post.thread=thread_id
WHERE thread_fts MATCH ?
GROUP BY thread_id, post_id
HAVING thread_id NOT IN (
SELECT thread
FROM post_fts JOIN post USING(id)
WHERE post_fts MATCH ?
)
LIMIT ? OFFSET (? - 1) * ?
)
JOIN thread_fts ON thread_fts.id=thread_id
JOIN person ON person.id=author_id
WHERE thread_fts MATCH ?
UNION
-- the main query, selects posts
SELECT
thread.id AS thread_id,
thread.name AS thread,
post.id AS post_id,
CASE what WHEN 1
THEN snippet(post_fts, '<b>', '</b>', '...', what)
ELSE post_fts.header END AS header,
CASE what WHEN 2
THEN snippet(post_fts, '**', '**', '...', what, -45)
ELSE SUBSTR(post_fts.content, 1, 200) END AS content,
person.name AS author,
cdate,
post.author AS author_id,
person.email AS email,
what
FROM post_fts JOIN (
-- inner query, selects ids of matching posts, orders and limits them,
-- so snippets only for limited count of posts are created (in outer query)
SELECT id, post.creation AS cdate, thread, 1 AS what, post.author AS author
FROM post_fts JOIN post USING(id)
WHERE post_fts.header MATCH ?
GROUP BY post.header
HAVING SUBSTR(post.header,1,3)<>'Re:'
UNION
SELECT id, post.creation AS cdate, thread, 2 AS what, post.author AS author
FROM post_fts JOIN post USING(id)
WHERE post_fts.content MATCH ?
ORDER BY what, cdate DESC
LIMIT ? OFFSET (? - 1) * ?
) AS post USING(id)
JOIN thread ON thread.id=thread
JOIN person ON person.id=author
WHERE post_fts MATCH ?
ORDER BY what ASC, cdate DESC
LIMIT 300 -- hardcoded limit just in case
;

247
main.tmpl
View file

@ -10,87 +10,170 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<title>${XmlEncode(title)}</title> <title>${xmlEncode(title)}</title>
<link rel="stylesheet" href="${c.req.makeUri("css/normalize.css", absolute = false)}"> <link rel="stylesheet" href="${c.req.makeUri("css/style.css", absolute = false)}">
<link rel="stylesheet" href="${c.req.makeUri("css/style.css", absolute = false)}">${additional_headers} <link rel="shortcut icon" href="${c.req.makeUri("favicon.ico", absolute = false)}">${additional_headers}
<meta charset="UTF-8"> <meta charset="UTF-8">
</head> </head>
<body> <body>
<div id="wrapper"> <!--- #head --->
#let frontQuery = c.req.makeUri("/") #let frontQuery = c.req.makeUri("/")
<div id="nimbtn"> <header id="head" class="forum">
<a href="${frontQuery}">Forum</a> <div class="wide-layout tall">
<div id="head-logo"></div>
<a id="head-logo-link" href="/"></a>
<nav id="head-links">
<a href="http://nim-lang.org/">home</a>
<a href="http://nim-lang.org/documentation.html">docs</a>
<a href="http://nim-lang.org/learn.html">learn</a>
<a href="http://nim-lang.org/download.html">download</a>
<a href="${frontQuery}" class="active">forum</a>
<a href="http://nim-lang.org/question.html">faq</a>
</nav>
</div> </div>
</header>
<div id="header"> <!--- #neck --->
<span>Nimrod's <a href="http://nimrod-lang.org/">homepage</a></span> <section id="neck">
<span><a href="http://nimrod-lang.org/documentation.html">stable docs</a></span> <div class="wide-layout tall">
<span><a href="http://build.nimrod-lang.org/docs/overview.html">development docs</a></span> <div style="right: 131px;" id="glow-arrow" class="forum"></div>
<span><a href="https://github.com/Araq/Nimrod/issues">github issues</a></span>
#if c.loggedIn:
<a href="${frontQuery}logout" class="right">Logout</a>
#let profileUrl = c.req.makeUri("profile/", false) &
# XMLencode(c.username)
<span id="welcome"><a href="${profileUrl}">$c.username</a></span>
<a href="$profileUrl">${genGravatar(c.email, 26)}</a>
#else:
<a href="${frontQuery}register" class="right">Register</a>
<a href="${frontQuery}login" class="right">Login</a>
#end if
</div> </div>
</section>
<div id="topbar"> <!--- #body --->
${c.genActionMenu} <section id="body" class="forum">
<div id="body-border"></div>
<div id="body-border-left"></div>
<div id="body-border-right"></div>
<div id="body-border-bottom"></div>
<div id="glow-line"></div>
<div id="glow-line-bottom"></div>
<div class="talk-layout">
<article id="content">
${content}
#if c.isThreadsList:
<div id="talk-info">
<div class="info">
<div>
${c.genListOnline(stats)}
</div>
</div>
</div>
#if not c.noPagenumumNav:
<div id="talk-nav">
${genPagenumNav(c, stats)}
</div>
#end if
#elif hasReplyBtn(c):
<div id="talk-info">
<div class="info-post">
<div>
${genPagenumNav(c, stats)}
</div>
</div>
<div class="user-post">
#if c.loggedIn():
#let replyUri = c.req.makeUri(c.req.path & "?action=reply#reply")
<a href="$replyUri">
<div>
<span class="reply">Reply</span>
</div>
</a>
#end if
</div>
</div>
#end if
</article>
<div id="sidebar">
<div class="title">Search</div>
<div class="content">
#if isFTSAvailable:
<a href="/search-help" target="_blank" class="searchHelp">?</a>
<form method="post" action="/search" class="searchForm">
<input type="text" name="q" maxlength="255" value="${c.search}" title="Search this forum" />
<input type="submit" value="Search" class="button search">
</form>
#else:
<form method="get" action="http://www.google.com/search" target="_blank" class="searchForm">
<input type="text" name="q" maxlength="255" value="${c.search}" title="Search this forum" />
<input type="submit" value="Search" class="button search">
<input type="hidden" name="sitesearch" value="http://forum.nimrod-lang.org" />
</form>
#end if
</div>
#if c.loggedIn:
<div class="title">Your account</div>
<div class="content user">
#let profileUrl = c.req.makeUri("profile/", false) &
# xmlEncode(c.username)
<a href="$profileUrl" class="user">${c.username}</a>
<a href="$profileUrl" class="avatar">${genGravatar(c.email)}</a>
<a href="${frontQuery}newthread" class="button">New Thread</a>
<a href="$profileUrl" class="button">My Profile</a>
<a href="${frontQuery}logout" class="button logout">Logout</a>
</div>
#else:
<div class="title">Login</div>
<div class="content">
<form name="login" action="${frontQuery}dologin" method="POST">
<span>Username: </span><input type="text" name="name" />
<span>Password: </span><input type="password" name="password" />
<input type="submit" style="display: none;"
id="hdnLogin" value="Login" />
</form>
#if c.errorMsg != "" and c.req.pathInfo.normalizeUri == "/dologin":
<span class="error">$c.errorMsg</span>
#end if
<a href="${frontQuery}register" class="button"
style="float: left;">Register</a>
<a href="#" onclick="document.forms['login'].submit()"
class="button">Login</a>
</div>
#end if
</div>
</div> </div>
</section>
<form method="get" action="http://www.google.com/search"> <!--- #foot --->
<table align="right" cellpadding="5pt" cellspacing="5pt"><tr><td> <footer id="foot" class="forum">
<input type="text" name="q" size="25" maxlength="255" value="" /> <div class="talk-layout tall">
</td><td> <div id="foot-links">
<input type="submit" value="Google Search this forum" /> <div>
<input type="hidden" name="sitesearch" <h4>Documentation</h4>
value="http://forum.nimrod-lang.org" /> <a href="http://nim-lang.org/documentation.html">Stable Documentation</a>
</td></tr></table> <a href="https://github.com/Araq/Nim/issues">Github Issues &amp; Requests</a>
</form> </div>
<div>
<div id="content"> <h4>Community</h4>
$content <a href="http://forum.nim-lang.org">User Forum</a>
<span style="color:red">$c.errorMsg</span> <a href="http://webchat.freenode.net/?channels=nim">Online IRC</a>
</div> <a href="http://irclogs.nim-lang.org/">IRC Logs</a>
#if c.req.pathInfo.normalizeUri notin noPageNums: </div>
${c.genPagenumNav(stats)} </div>
#end if <div id="foot-legal">
<div id="topbar"> <h4>Written in Nim - Powered by <a href="https://github.com/dom96/jester">Jester</a></h4>
${c.genActionMenu} Web Design by <a href="http://reign-studios.net/philipwitte/">Philip Witte</a> &amp; <a href="http://picheta.me">Dominik Picheta</a><br>
Copyright © 2015 - <a href="http://nim-lang.org/blog/">Andreas Rumpf</a> &amp; <a href="https://github.com/Araq/Nimrod/graphs/contributors">Contributors</a>
</div>
</div> </div>
</footer>
#if c.isThreadsList: <script>
${c.genListOnline(stats)} (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
#end if (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
#if showRssLinks: ga('create', 'UA-58103537-1', 'auto');
<span id="rss"> ga('send', 'pageview');
<a href="${c.req.makeUri("/threadActivity.xml")}"
><img
src="${c.req.makeUri("/images/Feed-icon.svg", absolute = false)}"
class="rssfeed"
>Thread activity</a>
<a href="${c.req.makeUri("/postActivity.xml")}"
><img
src="${c.req.makeUri("/images/Feed-icon.svg", absolute = false)}"
class="rssfeed"
>Posts activity</a>
</span>
#end if
<div id="footerPush"></div> </script>
</div>
<div id="footer"> <script src="${frontQuery}js/arrow.js"></script>
<span>Written in <a href="http://nimrod-lang.org/">Nimrod</a> using <a href="https://github.com/dom96/jester">Jester</a></span> <script src="${frontQuery}js/forum.js"></script>
<span> | <a href="https://github.com/nimrod-code/nimforum">Fork on Github</a></span>
<span> | User contributions licensed under <a href="http://creativecommons.org/licenses/by-sa/3.0/">cc-wiki</a> with <a href="/license">attribution required</a></span>
<span style="float:right;">Generated in ${int((epochTime()-c.startTime)*1000.0)}ms</span>
</div>
</body> </body>
</html> </html>
#end proc #end proc
@ -120,7 +203,7 @@
# const postContent = 5 # const postContent = 5
# const postId = 6 # const postId = 6
# let frontQuery = c.req.makeUri("/") # let frontQuery = c.req.makeUri("/")
# let recent = GetValue(db, sql"""SELECT # let recent = getValue(db, sql"""SELECT
# strftime('%Y-%m-%dT%H:%M:%SZ', (modified)) FROM thread # strftime('%Y-%m-%dT%H:%M:%SZ', (modified)) FROM thread
# ORDER BY modified DESC LIMIT 1""") # ORDER BY modified DESC LIMIT 1""")
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
@ -130,22 +213,22 @@
<link href="${frontQuery}" /> <link href="${frontQuery}" />
<id>${frontQuery}</id> <id>${frontQuery}</id>
<updated>${recent}</updated> <updated>${recent}</updated>
# for row in Rows(db, query, 10): # for row in rows(db, query, 10):
<entry> <entry>
<title>${XMLencode(%name)}</title> <title>${xmlEncode(%name)}</title>
<id>urn:entry:${%threadid}</id> <id>urn:entry:${%threadid}</id>
# let url = c.genThreadUrl(threadid = %threadid, # let url = c.genThreadUrl(threadid = %threadid,
# pageNum = $(ceil(parseInt(%postCount) / postsPerPage).int)) & # pageNum = $(ceil(parseInt(%postCount) / PostsPerPage).int)) &
# "#" & %postId # "#" & %postId
<link rel="alternate" type="text/html" <link rel="alternate" type="text/html"
href="${c.req.makeUri(url)}"/> href="${c.req.makeUri(url)}"/>
<published>${%threadDate}</published> <published>${%threadDate}</published>
<updated>${%threadDate}</updated> <updated>${%threadDate}</updated>
<author><name>${XMLEncode(%postAuthor)}</name></author> <author><name>${xmlEncode(%postAuthor)}</name></author>
<content type="html" <content type="html"
>Posts ${%postCount}, ${XMLEncode(%postAuthor)} said: >Posts ${%postCount}, ${xmlEncode(%postAuthor)} said:
&lt;p&gt; &lt;p&gt;
${XMLEncode(rstToHtml(%postContent))}</content> ${xmlEncode(rstToHtml(%postContent))}</content>
</entry> </entry>
# end for # end for
</feed> </feed>
@ -169,7 +252,7 @@ ${XMLEncode(rstToHtml(%postContent))}</content>
# const postHumanDate = 6 # const postHumanDate = 6
# const postPosition = 7 # const postPosition = 7
# let frontQuery = c.req.makeUri("/") # let frontQuery = c.req.makeUri("/")
# let recent = GetValue(db, sql"""SELECT # let recent = getValue(db, sql"""SELECT
# strftime('%Y-%m-%dT%H:%M:%SZ', creation) FROM post # strftime('%Y-%m-%dT%H:%M:%SZ', creation) FROM post
# ORDER BY creation DESC LIMIT 1""") # ORDER BY creation DESC LIMIT 1""")
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
@ -179,22 +262,22 @@ ${XMLEncode(rstToHtml(%postContent))}</content>
<link href="${frontQuery}" /> <link href="${frontQuery}" />
<id>${frontQuery}</id> <id>${frontQuery}</id>
<updated>${recent}</updated> <updated>${recent}</updated>
# for row in Rows(db, query, 10): # for row in rows(db, query, 10):
<entry> <entry>
<title>${XMLencode(%postHeader)}</title> <title>${xmlEncode(%postHeader)}</title>
<id>urn:entry:${%postId}</id> <id>urn:entry:${%postId}</id>
# let url = c.genThreadUrl(threadid = %postThread, # let url = c.genThreadUrl(threadid = %postThread,
# pageNum = $(ceil(parseInt(%postPosition) / postsPerPage).int)) & # pageNum = $(ceil(parseInt(%postPosition) / PostsPerPage).int)) &
# "#" & %postId # "#" & %postId
<link rel="alternate" type="text/html" <link rel="alternate" type="text/html"
href="${c.req.makeUri(url)}"/> href="${c.req.makeUri(url)}"/>
<published>${%postRssDate}</published> <published>${%postRssDate}</published>
<updated>${%postRssDate}</updated> <updated>${%postRssDate}</updated>
<author><name>${XMLEncode(%postAuthor)}</name></author> <author><name>${xmlEncode(%postAuthor)}</name></author>
<content type="html" <content type="html"
>On ${XMLEncode(%postHumanDate)}, ${XMLEncode(%postAuthor)} said: >On ${xmlEncode(%postHumanDate)}, ${xmlEncode(%postAuthor)} said:
&lt;p&gt; &lt;p&gt;
${XMLEncode(rstToHtml(%postContent))}</content> ${xmlEncode(rstToHtml(%postContent))}</content>
</entry> </entry>
# end for # end for
</feed> </feed>

11
nimforum.babel Normal file
View file

@ -0,0 +1,11 @@
[Package]
name = "nimforum"
version = "0.1.0"
author = "Dominik Picheta"
description = "Nimrod forum"
license = "MIT"
bin = "forum"
[Deps]
Requires: "nimrod >= 0.9.2, cairo#head, jester#head, bcrypt#head"

12
public/css/arrow.js Normal file
View file

@ -0,0 +1,12 @@
"use strict";
function positionGlowArrow() {
var headLinks = document.getElementById("head-links");
var activeLink = headLinks.getElementsByClassName("active")[0]
if (activeLink == undefined || activeLink == null)
return;
var offset = (headLinks.offsetWidth - activeLink.offsetLeft) - (activeLink.offsetWidth / 2) - 133;
var glowArrow = document.getElementById("glow-arrow");
glowArrow.style.right = offset + "px";
}

5
public/css/forum.js Normal file
View file

@ -0,0 +1,5 @@
"use strict";
window.onload = function() {
positionGlowArrow();
};

View file

@ -1,284 +0,0 @@
/*
* HTML5 Boilerplate
*
* What follows is the result of much research on cross-browser styling.
* Credit left inline and big thanks to Nicolas Gallagher, Jonathan Neal,
* Kroc Camen, and the H5BP dev community and team.
*
* Detailed information about this CSS: h5bp.com/css
*
* ==|== normalize ==========================================================
*/
/* =============================================================================
HTML5 display definitions
========================================================================== */
article, aside, details, figcaption, figure, footer, header, hgroup, nav, section { display: block; }
audio, canvas, video { display: inline-block; *display: inline; *zoom: 1; }
audio:not([controls]) { display: none; }
[hidden] { display: none; }
/* =============================================================================
Base
========================================================================== */
/*
* 1. Correct text resizing oddly in IE6/7 when body font-size is set using em units
* 2. Prevent iOS text size adjust on device orientation change, without disabling user zoom: h5bp.com/g
*/
html { font-size: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
html, button, input, select, textarea { font-family: sans-serif; color: #222; }
body { margin: 0; font-size: 1em; line-height: 1.4; }
/* =============================================================================
Links
========================================================================== */
a { color: #00e; }
a:visited { color: #551a8b; }
a:hover { color: #06e; }
a:focus { outline: thin dotted; }
/* Improve readability when focused and hovered in all browsers: h5bp.com/h */
a:hover, a:active { outline: 0; }
/* =============================================================================
Typography
========================================================================== */
abbr[title] { border-bottom: 1px dotted; }
b, strong { font-weight: bold; }
blockquote { margin: 1em 40px; }
dfn { font-style: italic; }
hr { display: block; height: 1px; border: 0; border-top: 1px solid #ccc; margin: 1em 0; padding: 0; }
ins { background: #ff9; color: #000; text-decoration: none; }
mark { background: #ff0; color: #000; font-style: italic; font-weight: bold; }
/* Redeclare monospace font family: h5bp.com/j */
pre, code, kbd, samp { font-family: monospace, serif; _font-family: 'courier new', monospace; font-size: 1em; }
/* Improve readability of pre-formatted text in all browsers */
pre { white-space: pre; white-space: pre-wrap; word-wrap: break-word; }
q { quotes: none; }
q:before, q:after { content: ""; content: none; }
small { font-size: 85%; }
/* Position subscript and superscript content without affecting line-height: h5bp.com/k */
sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; }
sup { top: -0.5em; }
sub { bottom: -0.25em; }
/* =============================================================================
Lists
========================================================================== */
ul, ol { margin: 1em 0; padding: 0 0 0 40px; }
dd { margin: 0 0 0 40px; }
nav ul, nav ol { list-style: none; list-style-image: none; margin: 0; padding: 0; }
/* =============================================================================
Embedded content
========================================================================== */
/*
* 1. Improve image quality when scaled in IE7: h5bp.com/d
* 2. Remove the gap between images and borders on image containers: h5bp.com/i/440
*/
img { border: 0; -ms-interpolation-mode: bicubic; vertical-align: middle; }
/*
* Correct overflow not hidden in IE9
*/
svg:not(:root) { overflow: hidden; }
/* =============================================================================
Figures
========================================================================== */
figure { margin: 0; }
/* =============================================================================
Forms
========================================================================== */
form { margin: 0; }
fieldset { border: 0; margin: 0; padding: 0; }
/* Indicate that 'label' will shift focus to the associated form element */
label { cursor: pointer; }
/*
* 1. Correct color not inheriting in IE6/7/8/9
* 2. Correct alignment displayed oddly in IE6/7
*/
legend { border: 0; *margin-left: -7px; padding: 0; white-space: normal; }
/*
* 1. Correct font-size not inheriting in all browsers
* 2. Remove margins in FF3/4 S5 Chrome
* 3. Define consistent vertical alignment display in all browsers
*/
button, input, select, textarea { font-size: 100%; margin: 0; vertical-align: baseline; *vertical-align: middle; }
/*
* 1. Define line-height as normal to match FF3/4 (set using !important in the UA stylesheet)
*/
button, input { line-height: normal; }
/*
* 1. Display hand cursor for clickable form elements
* 2. Allow styling of clickable form elements in iOS
* 3. Correct inner spacing displayed oddly in IE7 (doesn't effect IE6)
*/
button, input[type="button"], input[type="reset"], input[type="submit"] { cursor: pointer; -webkit-appearance: button; *overflow: visible; }
/*
* Re-set default cursor for disabled elements
*/
button[disabled], input[disabled] { cursor: default; }
/*
* Consistent box sizing and appearance
*/
input[type="checkbox"], input[type="radio"] { box-sizing: border-box; padding: 0; *width: 13px; *height: 13px; }
input[type="search"] { -webkit-appearance: textfield; -moz-box-sizing: content-box; -webkit-box-sizing: content-box; box-sizing: content-box; }
input[type="search"]::-webkit-search-decoration, input[type="search"]::-webkit-search-cancel-button { -webkit-appearance: none; }
/*
* Remove inner padding and border in FF3/4: h5bp.com/l
*/
button::-moz-focus-inner, input::-moz-focus-inner { border: 0; padding: 0; }
/*
* 1. Remove default vertical scrollbar in IE6/7/8/9
* 2. Allow only vertical resizing
*/
textarea { overflow: auto; vertical-align: top; resize: vertical; }
/* Colors for form validity */
input:valid, textarea:valid { }
input:invalid, textarea:invalid { background-color: #f0dddd; }
/* =============================================================================
Tables
========================================================================== */
table { border-collapse: collapse; border-spacing: 0; }
td { vertical-align: top; }
/* =============================================================================
Chrome Frame Prompt
========================================================================== */
.chromeframe { margin: 0.2em 0; background: #ccc; color: black; padding: 0.2em 0; }
/* ==|== primary styles =====================================================
Author:
========================================================================== */
/* ==|== media queries ======================================================
EXAMPLE Media Query for Responsive Design.
This example overrides the primary ('mobile first') styles
Modify as content requires.
========================================================================== */
@media only screen and (min-width: 35em) {
/* Style adjustments for viewports that meet the condition */
}
/* ==|== non-semantic helper classes ========================================
Please define your styles before this section.
========================================================================== */
/* For image replacement */
.ir { display: block; border: 0; text-indent: -999em; overflow: hidden; background-color: transparent; background-repeat: no-repeat; text-align: left; direction: ltr; *line-height: 0; }
.ir br { display: none; }
/* Hide from both screenreaders and browsers: h5bp.com/u */
.hidden { display: none !important; visibility: hidden; }
/* Hide only visually, but have it available for screenreaders: h5bp.com/v */
.visuallyhidden { border: 0; clip: rect(0 0 0 0); height: 1px; margin: -1px; overflow: hidden; padding: 0; position: absolute; width: 1px; }
/* Extends the .visuallyhidden class to allow the element to be focusable when navigated to via the keyboard: h5bp.com/p */
.visuallyhidden.focusable:active, .visuallyhidden.focusable:focus { clip: auto; height: auto; margin: 0; overflow: visible; position: static; width: auto; }
/* Hide visually and from screenreaders, but maintain layout */
.invisible { visibility: hidden; }
/* Contain floats: h5bp.com/q */
.clearfix:before, .clearfix:after { content: ""; display: table; }
.clearfix:after { clear: both; }
.clearfix { *zoom: 1; }
/* ==|== print styles =======================================================
Print styles.
Inlined to avoid required HTTP connection: h5bp.com/r
========================================================================== */
@media print {
* { background: transparent !important; color: black !important; box-shadow:none !important; text-shadow: none !important; filter:none !important; -ms-filter: none !important; } /* Black prints faster: h5bp.com/s */
a, a:visited { text-decoration: underline; }
a[href]:after { content: " (" attr(href) ")"; }
abbr[title]:after { content: " (" attr(title) ")"; }
.ir a:after, a[href^="javascript:"]:after, a[href^="#"]:after { content: ""; } /* Don't show links for images, or javascript/internal links */
pre, blockquote { border: 1px solid #999; page-break-inside: avoid; }
thead { display: table-header-group; } /* h5bp.com/t */
tr, img { page-break-inside: avoid; }
img { max-width: 100% !important; }
@page { margin: 0.5cm; }
p, h2, h3 { orphans: 3; widows: 3; }
h2, h3 { page-break-after: avoid; }
}

File diff suppressed because it is too large Load diff

BIN
public/images/bg.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 490 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 424 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

BIN
public/images/glow-line.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
public/images/head-link.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 799 B

BIN
public/images/head.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 B

BIN
public/images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 897 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

12
public/js/arrow.js Normal file
View file

@ -0,0 +1,12 @@
"use strict";
function positionGlowArrow() {
var headLinks = document.getElementById("head-links");
var activeLink = headLinks.getElementsByClassName("active")[0]
if (activeLink == undefined || activeLink == null)
return;
var offset = (headLinks.offsetWidth - activeLink.offsetLeft) - (activeLink.offsetWidth / 2) - 133;
var glowArrow = document.getElementById("glow-arrow");
glowArrow.style.right = offset + "px";
}

5
public/js/forum.js Normal file
View file

@ -0,0 +1,5 @@
"use strict";
window.onload = function() {
positionGlowArrow();
};

35
static/search-help.rst Normal file
View file

@ -0,0 +1,35 @@
Full-text search for Nim forum
==============================
Syntax (using *SQLite* dll compiled without *Enhanced Query Syntax* support):
-----------------------------------------------------------------------------
- Only alphanumeric characters are searched.
- Only full words and words beginnings (e.g. ``Nim*`` for both ``Nimrod`` and ``Nim``) are searched
- All words are joined with implicit **AND** operator; there's no explicit one
- There's explicit **OR** operator (upper-case) and it has higher priority
- Words can be prepended with **-** to be excluded from search
- No parentheses support
- Quotes for phrases search, e.g. ``"programming language"``
- Distances between words/phrases can be specified putting ``NEAR`` or ``NEAR/some_number`` between them
Syntax - differences in *Enhanced Query Syntax* (should be enabled in *SQLite* dll):
------------------------------------------------------------------------------------
- **AND** and **NOT** logical operators available
- Precedence of operators is, from highest to lowest: **NOT**, **AND**, **OR**
- Parentheses for grouping are supported
Where search is performed:
--------------------------
- **Threads' titles** - these results are outputed first
- **Posts' titles** - middle precedence
- **Posts' contents** - the latest
How results are shown:
----------------------
- All results are ordered by date (posts' edits don't affect)
- Matched tokens in text are marked (bold or dotted underline)
- Threads title is the link to the thread and posts title is the link to the post