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.
*.swp
nimcache/
*.db
# Specific paths
/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.} =
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)
proc createCaptcha*(file, text: string) =

View file

@ -8,7 +8,7 @@
import strutils, db_sqlite
var db = Open(connection="nimforum.db", user="postgres", password="",
var db = open(connection="nimforum.db", user="postgres", password="",
database="nimforum")
const
@ -16,7 +16,7 @@ const
TPassword = "varchar(32)"
TEmail = "varchar(30)"
db.Exec(sql"""
db.exec(sql"""
create table if not exists thread(
id integer primary key,
name varchar(100) not null,
@ -24,11 +24,11 @@ create table if not exists thread(
modified timestamp not null default (DATETIME('now'))
);""", [])
db.Exec(sql"""
db.exec(sql"""
create unique index if not exists ThreadNameIx on thread (name);
""", [])
db.Exec(sql("""
db.exec(sql("""
create table if not exists person(
id integer primary key,
name $# not null,
@ -42,14 +42,19 @@ create table if not exists person(
);""" % [TUserName, TPassword, TEmail]), [])
# 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);
""", [])
# ----------------------- Forum ------------------------------------------------
if not db.TryExec(sql"""
if not db.tryExec(sql"""
create table if not exists post(
id integer primary key,
author integer not null,
@ -66,7 +71,7 @@ create table if not exists post(
# -------------------- Session -------------------------------------------------
if not db.TryExec(sql("""
if not db.tryExec(sql("""
create table if not exists session(
id integer primary key,
ip inet not null,
@ -77,7 +82,7 @@ create table if not exists session(
);""" % [TPassword]), []):
echo "session table already exists"
if not db.TryExec(sql"""
if not db.tryExec(sql"""
create table if not exists antibot(
id integer primary key,
ip inet not null,
@ -86,8 +91,35 @@ create table if not exists antibot(
);""", []):
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()
Close(db)
close(db)

View file

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

741
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"
--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
;

257
main.tmpl
View file

@ -10,87 +10,170 @@
<!doctype html>
<html lang="en">
<head>
<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)}">${additional_headers}
<title>${xmlEncode(title)}</title>
<link rel="stylesheet" href="${c.req.makeUri("css/style.css", absolute = false)}">
<link rel="shortcut icon" href="${c.req.makeUri("favicon.ico", absolute = false)}">${additional_headers}
<meta charset="UTF-8">
</head>
<body>
<div id="wrapper">
#let frontQuery = c.req.makeUri("/")
<div id="nimbtn">
<a href="${frontQuery}">Forum</a>
<!--- #head --->
#let frontQuery = c.req.makeUri("/")
<header id="head" class="forum">
<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 id="header">
<span>Nimrod's <a href="http://nimrod-lang.org/">homepage</a></span>
<span><a href="http://nimrod-lang.org/documentation.html">stable docs</a></span>
<span><a href="http://build.nimrod-lang.org/docs/overview.html">development docs</a></span>
<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
</header>
<!--- #neck --->
<section id="neck">
<div class="wide-layout tall">
<div style="right: 131px;" id="glow-arrow" class="forum"></div>
</div>
<div id="topbar">
${c.genActionMenu}
</section>
<!--- #body --->
<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>
<form method="get" action="http://www.google.com/search">
<table align="right" cellpadding="5pt" cellspacing="5pt"><tr><td>
<input type="text" name="q" size="25" maxlength="255" value="" />
</td><td>
<input type="submit" value="Google Search this forum" />
<input type="hidden" name="sitesearch"
value="http://forum.nimrod-lang.org" />
</td></tr></table>
</form>
<div id="content">
$content
<span style="color:red">$c.errorMsg</span>
</div>
#if c.req.pathInfo.normalizeUri notin noPageNums:
${c.genPagenumNav(stats)}
#end if
<div id="topbar">
${c.genActionMenu}
</section>
<!--- #foot --->
<footer id="foot" class="forum">
<div class="talk-layout tall">
<div id="foot-links">
<div>
<h4>Documentation</h4>
<a href="http://nim-lang.org/documentation.html">Stable Documentation</a>
<a href="https://github.com/Araq/Nim/issues">Github Issues &amp; Requests</a>
</div>
<div>
<h4>Community</h4>
<a href="http://forum.nim-lang.org">User Forum</a>
<a href="http://webchat.freenode.net/?channels=nim">Online IRC</a>
<a href="http://irclogs.nim-lang.org/">IRC Logs</a>
</div>
</div>
<div id="foot-legal">
<h4>Written in Nim - Powered by <a href="https://github.com/dom96/jester">Jester</a></h4>
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>
</footer>
#if c.isThreadsList:
${c.genListOnline(stats)}
#end if
#if showRssLinks:
<span id="rss">
<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>
</div>
<div id="footer">
<span>Written in <a href="http://nimrod-lang.org/">Nimrod</a> using <a href="https://github.com/dom96/jester">Jester</a></span>
<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>
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(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');
ga('create', 'UA-58103537-1', 'auto');
ga('send', 'pageview');
</script>
<script src="${frontQuery}js/arrow.js"></script>
<script src="${frontQuery}js/forum.js"></script>
</body>
</html>
#end proc
@ -120,7 +203,7 @@
# const postContent = 5
# const postId = 6
# 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
# ORDER BY modified DESC LIMIT 1""")
<?xml version="1.0" encoding="utf-8"?>
@ -130,22 +213,22 @@
<link href="${frontQuery}" />
<id>${frontQuery}</id>
<updated>${recent}</updated>
# for row in Rows(db, query, 10):
# for row in rows(db, query, 10):
<entry>
<title>${XMLencode(%name)}</title>
<title>${xmlEncode(%name)}</title>
<id>urn:entry:${%threadid}</id>
# let url = c.genThreadUrl(threadid = %threadid,
# pageNum = $(ceil(parseInt(%postCount) / postsPerPage).int)) &
# pageNum = $(ceil(parseInt(%postCount) / PostsPerPage).int)) &
# "#" & %postId
<link rel="alternate" type="text/html"
href="${c.req.makeUri(url)}"/>
<published>${%threadDate}</published>
<updated>${%threadDate}</updated>
<author><name>${XMLEncode(%postAuthor)}</name></author>
<author><name>${xmlEncode(%postAuthor)}</name></author>
<content type="html"
>Posts ${%postCount}, ${XMLEncode(%postAuthor)} said:
>Posts ${%postCount}, ${xmlEncode(%postAuthor)} said:
&lt;p&gt;
${XMLEncode(rstToHtml(%postContent))}</content>
${xmlEncode(rstToHtml(%postContent))}</content>
</entry>
# end for
</feed>
@ -169,7 +252,7 @@ ${XMLEncode(rstToHtml(%postContent))}</content>
# const postHumanDate = 6
# const postPosition = 7
# 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
# ORDER BY creation DESC LIMIT 1""")
<?xml version="1.0" encoding="utf-8"?>
@ -179,22 +262,22 @@ ${XMLEncode(rstToHtml(%postContent))}</content>
<link href="${frontQuery}" />
<id>${frontQuery}</id>
<updated>${recent}</updated>
# for row in Rows(db, query, 10):
# for row in rows(db, query, 10):
<entry>
<title>${XMLencode(%postHeader)}</title>
<title>${xmlEncode(%postHeader)}</title>
<id>urn:entry:${%postId}</id>
# let url = c.genThreadUrl(threadid = %postThread,
# pageNum = $(ceil(parseInt(%postPosition) / postsPerPage).int)) &
# pageNum = $(ceil(parseInt(%postPosition) / PostsPerPage).int)) &
# "#" & %postId
<link rel="alternate" type="text/html"
href="${c.req.makeUri(url)}"/>
<published>${%postRssDate}</published>
<updated>${%postRssDate}</updated>
<author><name>${XMLEncode(%postAuthor)}</name></author>
<author><name>${xmlEncode(%postAuthor)}</name></author>
<content type="html"
>On ${XMLEncode(%postHumanDate)}, ${XMLEncode(%postAuthor)} said:
>On ${xmlEncode(%postHumanDate)}, ${xmlEncode(%postAuthor)} said:
&lt;p&gt;
${XMLEncode(rstToHtml(%postContent))}</content>
${xmlEncode(rstToHtml(%postContent))}</content>
</entry>
# end for
</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