diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..fde1b09 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,80 @@ +# This is a basic workflow to help you get started with Actions + +name: CI + +# Controls when the action will run. +on: + # Triggers the workflow on push or pull request events but only for the master branch + push: + branches: [ master ] + pull_request: + branches: [ master ] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + test_stable: + runs-on: ubuntu-latest + strategy: + matrix: + firefox: [ '73.0' ] + include: + - nim-version: 'stable' + cache-key: 'stable' + steps: + - uses: actions/checkout@v2 + - name: Checkout submodules + run: git submodule update --init --recursive + + - name: Setup firefox + uses: browser-actions/setup-firefox@latest + with: + firefox-version: ${{ matrix.firefox }} + + - name: Get Date + id: get-date + run: echo "::set-output name=date::$(date "+%Y-%m-%d")" + shell: bash + + - name: Cache choosenim + uses: actions/cache@v2 + with: + path: ~/.choosenim + key: ${{ runner.os }}-choosenim-${{ matrix.cache-key }} + + - name: Cache nimble + uses: actions/cache@v2 + with: + path: ~/.nimble + key: ${{ runner.os }}-nimble-${{ hashFiles('*.nimble') }} + + - uses: jiro4989/setup-nim-action@v1 + with: + nim-version: "${{ matrix.nim-version }}" + + - name: Install geckodriver + run: | + sudo apt-get -qq update + sudo apt-get install autoconf libtool libsass-dev + + wget https://github.com/mozilla/geckodriver/releases/download/v0.29.1/geckodriver-v0.29.1-linux64.tar.gz + mkdir geckodriver + tar -xzf geckodriver-v0.29.1-linux64.tar.gz -C geckodriver + export PATH=$PATH:$PWD/geckodriver + + - name: Install choosenim + run: | + export CHOOSENIM_CHOOSE_VERSION="stable" + curl https://nim-lang.org/choosenim/init.sh -sSf > init.sh + sh init.sh -y + export PATH=$HOME/.nimble/bin:$PATH + nimble refresh -y + + - name: Run tests + run: | + export MOZ_HEADLESS=1 + nimble -y install + nimble -y test + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fe26a8a --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +# Wildcard patterns. +*.swp +nimcache/ +*.db* + +# Specific paths +/createdb +/forum +/nimforum.db + +# Binaries +forum +createdb +editdb + +.vscode +forum.json* +browsertester +setup_nimforum +buildcss +nimforum.css + +/src/frontend/forum.js diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..6ea9ea9 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "frontend/spectre"] + path = frontend/spectre + url = https://github.com/picturepan2/spectre +[submodule "public/css/spectre"] + path = public/css/spectre + url = https://github.com/picturepan2/spectre diff --git a/README.md b/README.md index abacfe4..d7dedb4 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,131 @@ -nimforum -======== +# nimforum -This is Nimrod's forum. The code is not nice and depends on the RST parser of -the Nimrod compiler. +NimForum is a light-weight forum implementation +with many similarities to Discourse. It is implemented in +the [Nim](https://nim-lang.org) programming +language and uses SQLite for its database. +## Examples in the wild + +[![forum.nim-lang.org](https://i.imgur.com/hdIF5Az.png)](https://forum.nim-lang.org) + +

forum.nim-lang.org

+ +## Features + +* Efficient, type safe and clean **single-page application** developed using the + [Karax](https://github.com/pragmagic/karax) and + [Jester](https://github.com/dom96/jester) frameworks. +* **Utilizes SQLite** making set up much easier. +* Endlessly **customizable** using SASS. +* Spam blocking via new user sandboxing with great tools for moderators. +* reStructuredText enriched by Markdown to make formatting your posts a breeze. +* Search powered by SQLite's full-text search. +* Context-aware replies. +* Last visit tracking. +* Gravatar support. +* And much more! + +## Setup + +[See this document.](https://github.com/nim-lang/nimforum/blob/master/setup.md) + +## Dependencies + +The following lists the dependencies which you may need to install manually +in order to get NimForum running, compiled*, or tested†. + +* libsass +* SQLite +* pcre +* Nim (and the Nimble package manager)* +* [geckodriver](https://github.com/mozilla/geckodriver)† + * Firefox† + +[*] Build time dependencies + +[†] Test time dependencies + +## Development + +Check out the tasks defined by this project's ``nimforum.nimble`` file by +running ``nimble tasks``, as of writing they are: + +``` +backend Compiles and runs the forum backend +runbackend Runs the forum backend +frontend Builds the necessary JS frontend (with CSS) +minify Minifies the JS using Google's closure compiler +testdb Creates a test DB (with admin account!) +devdb Creates a test DB (with admin account!) +blankdb Creates a blank DB +test Runs tester +fasttest Runs tester without recompiling backend +``` + +To get up and running: + +```bash +git clone https://github.com/nim-lang/nimforum +cd nimforum +git submodule update --init --recursive + +# Setup the db with user: admin, pass: admin and some other users +nimble devdb + +# Run this again if frontend code changes +nimble frontend + +# Will start a server at localhost:5000 +nimble backend +``` + +Development typically involves running `nimble devdb` which sets up the +database for development and testing, then `nimble backend` +which compiles and runs the forum's backend, and `nimble frontend` +separately to build the frontend. When making changes to the frontend it +should be enough to simply run `nimble frontend` again to rebuild. This command +will also build the SASS ``nimforum.scss`` file in the `public/css` directory. + +### With docker + +You can easily launch site on localhost if you have `docker` and `docker-compose`. +You don't have to setup dependencies (libsass, sglite, pcre, etc...) on you host PC. + +To get up and running: + +```bash +cd docker +docker-compose build +docker-compose up +``` + +And you can access local NimForum site. +Open http://localhost:5000 . + +# Troubleshooting + +You might have to run `nimble install karax@#5f21dcd`, if setup fails +with: + +``` +andinus@circinus ~/projects/forks/nimforum> nimble --verbose devdb +[...] + Installing karax@#5f21dcd + Tip: 24 messages have been suppressed, use --verbose to show them. + Error: No binaries built, did you specify a valid binary name? +[...] + Error: Exception raised during nimble script execution +``` + +The hash needs to be replaced with the one specified in output. + +# Copyright + +Copyright (c) 2012-2018 Andreas Rumpf, Dominik Picheta. -Copyright (c) 2012 Andreas Rumpf. All rights reserved. +# License +NimForum is licensed under the MIT license. diff --git a/captchas.nim b/captchas.nim deleted file mode 100644 index 0d56022..0000000 --- a/captchas.nim +++ /dev/null @@ -1,39 +0,0 @@ -# -# -# The Nimrod Forum -# (c) Copyright 2012 Andreas Rumpf -# -# All rights reserved. -# - -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 = - result = req.makeUri("/captchas/capture_" & $i & ".png", absolute = false) - -proc createCaptcha*(file, text: string) = - var surface = imageSurfaceCreate(FORMAT_ARGB32, 10*text.len, 10) - var cr = create(surface) - - selectFontFace(cr, "serif", FONT_SLANT_NORMAL, FONT_WEIGHT_BOLD) - setFontSize(cr, 12.0) - - setSourceRgb(cr, 1.0, 0.5, 0.0) - moveTo(cr, 0.0, 10.0) - showText(cr, repeatChar(text.len, 'O')) - - setSourceRgb(cr, 0.0, 0.0, 1.0) - moveTo(cr, 0.0, 10.0) - showText(cr, text) - - destroy(cr) - discard writeToPng(surface, file) - destroy(surface) - -when isMainModule: - createCapture("test.png", "1+33") - - diff --git a/createdb.nim b/createdb.nim deleted file mode 100644 index 8cd3289..0000000 --- a/createdb.nim +++ /dev/null @@ -1,92 +0,0 @@ -# -# -# Nimrod Forum -# (c) Copyright 2012 Andreas Rumpf -# -# All rights reserved. -# - -import strutils, db_sqlite - -var db = Open(connection="nimforum.db", user="postgres", password="", - database="nimforum") - -const - TUserName = "varchar(20)" - TPassword = "varchar(32)" - TEmail = "varchar(30)" - -db.Exec(sql""" -create table if not exists thread( - id integer primary key, - name varchar(100) not null, - views integer not null, - modified timestamp not null default (DATETIME('now')) -);""", []) - -db.Exec(sql""" -create unique index if not exists ThreadNameIx on thread (name); -""", []) - -db.Exec(sql(""" -create table if not exists person( - id integer primary key, - name $# not null, - password $# not null, - email $# not null, - creation timestamp not null default (DATETIME('now')), - salt varbin(128) not null, - status integer not null, - admin bool default false -);""" % [TUserName, TPassword, TEmail]), []) -# echo "person table already exists" - -db.Exec(sql""" -create unique index if not exists UserNameIx on person (name); -""", []) - -# ----------------------- Forum ------------------------------------------------ - - -if not db.TryExec(sql""" -create table if not exists post( - id integer primary key, - author integer not null, - ip inet not null, - header varchar(100) not null, - content varchar(1000) not null, - thread integer not null, - creation timestamp not null default (DATETIME('now')), - - foreign key (thread) references thread(id), - foreign key (author) references person(id) -);""", []): - echo "post table already exists" - -# -------------------- Session ------------------------------------------------- - -if not db.TryExec(sql(""" -create table if not exists session( - id integer primary key, - ip inet not null, - password $# not null, - userid integer not null, - lastModified timestamp not null default (DATETIME('now')), - foreign key (userid) references person(id) -);""" % [TPassword]), []): - echo "session table already exists" - -if not db.TryExec(sql""" -create table if not exists antibot( - id integer primary key, - ip inet not null, - answer varchar(30) not null, - created timestamp not null default (DATETIME('now')) -);""", []): - echo "antibot table already exists" - -#discard stdin.readline() - -Close(db) - - diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..cb3191a --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,14 @@ +FROM nimlang/nim:1.2.6-ubuntu + +RUN apt-get update -yqq \ + && apt-get install -y --no-install-recommends \ + libsass-dev \ + sqlite3 \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app +COPY . /app + +# install dependencies +RUN nimble install -Y diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..8657235 --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,12 @@ +version: "3.7" + +services: + forum: + build: + context: ../ + dockerfile: ./docker/Dockerfile + volumes: + - "../:/app" + ports: + - "5000:5000" + entrypoint: "/app/docker/entrypoint.sh" diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100755 index 0000000..d8f5923 --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,19 @@ +#!/bin/sh + +set -eu + +git submodule update --init --recursive + +# setup +nimble c -d:release src/setup_nimforum.nim +./src/setup_nimforum --dev + +# build frontend +nimble c -r src/buildcss +nimble js -d:release src/frontend/forum.nim +mkdir -p public/js +cp src/frontend/forum.js public/js/forum.js + +# build backend +nimble c src/forum.nim +./src/forum diff --git a/editdb.nim b/editdb.nim deleted file mode 100644 index ea28f93..0000000 --- a/editdb.nim +++ /dev/null @@ -1,11 +0,0 @@ - -import strutils, db_sqlite - -var db = Open(connection="nimforum.db", user="postgres", password="", - database="nimforum") - -db.exec(sql"""ALTER TABLE person add column - lastOnline timestamp -""", []) - -close(db) \ No newline at end of file diff --git a/forms.tmpl b/forms.tmpl deleted file mode 100644 index 039b435..0000000 --- a/forms.tmpl +++ /dev/null @@ -1,218 +0,0 @@ -#! stdtmpl -# -#template `%`(idx: expr): expr {.immediate.} = -# row[idx] -#end template -# -# -#proc genThreadsList(c: var TForumData): string = -# const query = sql"select id, name, views, modified from thread order by modified desc" -# const threadId = 0 -# const name = 1 -# const views = 2 -# -# result = "" - - - - - - - - -# for row in Rows(db, query): - - - #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 posts = GetValue(db, sql"select count(*) from post where thread = ?", %threadId) - - - #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 latestReplyDate = getValue(db, sql("SELECT strftime('%s', " & - # "(select creation from post where id = (select max(id) from post where thread = ?)))"), %threadId) - - -# end for -
TopicsAuthorPostsViewsLast reply
${UrlButton(c, XMLencode(%name), c.genThreadUrl(threadid = %threadid))}${authorName}$posts${XMLencode(%views)} - ${formatTimestamp(latestReplyDate.parseInt())}
- ${latestReplyAuthor} -
-#end proc -# -# -#proc genPostPreview(c: var TForumData, -# title, content, author, date: string): string = -# result = "" - - - - - - - - -
- ${XMLEncode(title)} - ${XMLencode(date)} -
- ${XMLencode(author)} - - #try: - ${content.rstToHtml} - #except EParseError: - # c.errorMsg = getCurrentExceptionMsg() - #end -
-#end proc -# -# -#proc genPostsList(c: var TForumData, threadId: string): string = -# const query = sql"select p.id, u.name, p.header, p.content, p.creation, p.author, u.email from post p, person u where u.id = p.author and p.thread = ? order by p.id" -# const postId = 0 -# const userName = 1 -# const postHeader = 2 -# const postContent = 3 -# const postCreation = 4 -# const postAuthor = 5 -# const userEmail = 6 -# result = "" -# for row in FastRows(db, query, threadId): - - - - - - - - -
- ${XMLencode(%postHeader)} - ${XMLencode(%postCreation)} -
- ${XMLencode(%userName)} -
- ${genGravatar(%userEmail)} - #if c.userId == %postAuthor and c.currentPost.subject.len == 0: -
${UrlButton(c, "Edit post", c.genThreadUrl(%postId, "edit"))} - #elif c.isAdmin and c.currentPost.subject.len == 0: -
- ${UrlButton(c, "Edit post", c.genThreadUrl(%postId, "edit"))} - #end if -
- #try: - ${(%postContent).rstToHtml} - #except EParseError: - # c.errorMsg = getCurrentExceptionMsg() - #end -
-# end for -#end proc -# -# -#proc genFormPost(c: var TForumData, action: string, -# topText, title, content: string, isEdit: bool): string = -# result = "" -
- -
-
- ${topText} -
-
- ${FieldValid(c, "subject", "Subject:")} - ${TextWidget(c, "subject", title, maxlength=100)} -
- ${FieldValid(c, "content", "Content:")}
- ${TextAreaWidget(c, "content", content, width=100, height=20)}
- ${FormSession(c, action)} - - # if isEdit: - Delete Post
- # end if -
- - - - Syntax Cheatsheet -
-
-#end proc -# -# -#proc genFormRegister(c: var TForumData): string = -# result = "" -
- Register
- - - - - - - - - - - - - - - - - -
${FieldValid(c, "name", "Username:")}${TextWidget(c, "name", reuseText, maxlength=20)}
${FieldValid(c, "new_password", "Password:")}
${FieldValid(c, "email", "E-Mail:")}${TextWidget(c, "email", reuseText, maxlength=30)}
${FieldValid(c, "antibot", "What is " & antibot(c) & "?")}${TextWidget(c, "antibot", "", maxlength=4)}
- -
-#end proc -# -#proc genFormLogin(c: var TForumData): string = -# result = "" -# if not c.loggedIn: -
- - - -
Username: -
Password: -
- -
- $c.loginErrorMsg -# else: - You're already logged in! -# end if -#end proc -# -# -#proc genListOnline(c: var TForumData): string = -# result = "" -# let stats = c.getStats() -
-
- Who is online? -
-
- Out of ${stats.totalUsers} users ${stats.activeUsers.len} are online${if stats.activeUsers.len == 0: "." else: ":"} - #for index, usr in stats.activeUsers: - # if usr.isAdmin: - #if index != 0: result.add ',' - #end if - #result.add(""" """ & usr.nick & """""") - # else: - #if index != 0: result.add ',' - #end if - #result.add(""" """ & usr.nick & """""") - # end if - #end for - -
- Total threads: ${stats.totalThreads} | Total posts: ${stats.totalPosts} | Newest member: ${stats.newestMember.nick} -
-
- -#end proc diff --git a/forum.nim b/forum.nim deleted file mode 100644 index 1055b43..0000000 --- a/forum.nim +++ /dev/null @@ -1,580 +0,0 @@ -# -# -# The Nimrod Forum -# (c) Copyright 2012 Andreas Rumpf -# -# All rights reserved. -# - -import - os, strutils, times, md5, strtabs, cgi, math, db_sqlite, matchers, - rst, rstgen, captchas, sockets, scgi, jester - -const - unselectedThread = -1 - transientThread = 0 - -type - TCrud = enum crCreate, crRead, crUpdate, crDelete - - TSession = object of TObject - threadid: int - postid: int - userName, userPass, email: string - isAdmin: bool - - TPost = tuple[subject, content: string] - - TForumData = object of TSession - req: TRequest - userid: string - actionContent: string - errorMsg, loginErrorMsg: string - invalidField: string - currentPost: TPost - startTime: float - - TStyledButton = tuple[text: string, link: string] - - TForumStats = object - totalUsers: int - totalPosts: int - totalThreads: int - newestMember: tuple[nick: string, id: int, isAdmin: bool] - activeUsers: seq[tuple[nick: string, id: int, isAdmin: bool]] - -var - db: TDbConn - docConfig: PStringTable - -proc init(c: var TForumData) = - c.userPass = "" - c.userName = "" - c.threadId = unselectedThread - c.postId = -1 - - c.userid = "" - c.actionContent = "" - c.errorMsg = "" - c.loginErrorMsg = "" - c.invalidField = "" - c.currentPost = (subject: "", content: "") - -proc loggedIn(c: TForumData): bool = - result = c.userName.len > 0 - -# --------------- HTML widgets ------------------------------------------------ - -# for widgets "" means the empty string as usual; should the old value be -# used again, pass `reuseText` instead: -const - reuseText = "\1" - -proc TextWidget(c: TForumData, name, defaultText: string, - maxlength = 30, size = -1): string = - let x = if defaultText != reuseText: defaultText - else: XMLencode(c.req.params[name]) - return """""" % [ - name, $maxlength, x, if size != -1: "size=\"" & $size & "\"" else: ""] - -proc TextAreaWidget(c: TForumData, name, defaultText: string, - width = 80, height = 20): string = - let x = if defaultText != reuseText: defaultText - else: XMLencode(c.req.params[name]) - return """""" % [ - name, $width, $height, x] - -proc FieldValid(c: TForumData, name, text: string): string = - if name == c.invalidField: - result = """$1""" % text - else: - result = text - -proc genThreadUrl(c: TForumData, postId = "", action = "", threadid = ""): string = - result = "/t/" & (if threadid == "": $c.threadId else: threadid) - if action != "": - result.add("?action=" & action) - if postId != "": - result.add("&postid=" & postid) - result = c.req.makeUri(result, absolute = false) - -proc FormSession(c: var TForumData, nextAction: string): string = - return """ - """ % [ - $c.threadId, $c.postid] - -proc UrlButton(c: var TForumData, text, url: string): string = - return ("""$2""") % [ - url, text] - -proc genButtons(c: var TForumData, btns: seq[TStyledButton]): string = - if btns.len == 1: - var anchor = "" - - result = ("""$2""") % [ - btns[0].link, btns[0].text, anchor] - else: - result = "" - for i, btn in pairs(btns): - var anchor = "" - - var class = "" - if i == 0: class = "left " - elif i == btns.len()-1: class = "right " - else: class = "middle " - result.add(("""$2""") % [ - btns[i].link, btns[i].text, class, anchor]) - -proc formatTimestamp(t: int): string = - let t2 = getGMTime(TTime(t)) - return t2.format("ddd',' d MMM yyyy HH':'mm 'UTC'") - -proc genGravatar(email: string, size: int = 80): string = - let emailMD5 = email.toLower.toMD5 - result = "" % - ("http://www.gravatar.com/avatar/" & $emailMD5 & "?s=" & $size & - "&d=identicon") - -proc randomSalt(): string = - result = "" - for i in 0..127: - var r = random(225) - if r >= 32 and r <= 126: - result.add(chr(random(225))) - -proc devRandomSalt(): string = - when defined(posix): - result = "" - var f = open("/dev/urandom") - var randomBytes: array[0..127, char] - discard f.readBuffer(addr(randomBytes), 128) - for i in 0..127: - if ord(randomBytes[i]) >= 32 and ord(randomBytes[i]) <= 126: - result.add(randomBytes[i]) - f.close() - else: - result = randomSalt() - -proc makeSalt(): string = - ## Creates a salt using a cryptographically secure random number generator. - try: - result = devRandomSalt() - except EIO: - result = randomSalt() - -proc makePassword(password, salt: string): string = - ## Creates an MD5 hash by combining password and salt. - result = getMD5(salt & getMD5(password)) - -# ----------------------------------------------------------------------------- -template `||`(x: expr): expr = (if not isNil(x): x else: "") - -proc validThreadId(c: TForumData): bool = - result = GetValue(db, sql"select id from thread where id = ?", - $c.threadId).len > 0 - -proc antibot(c: var TForumData): string = - let a = math.random(10)+1 - let b = math.random(1000)+1 - let answer = $(a+b) - - Exec(db, sql"delete from antibot where ip = ?", c.req.ip) - let CaptchaId = TryInsertID(db, - sql"insert into antibot(ip, answer) values (?, ?)", c.req.ip, - answer).int mod 10_000 - let CaptchaFile = getCaptchaFilename(CaptchaId) - createCaptcha(CaptchaFile, $a & "+" & $b) - result = """""" % c.req.getCaptchaUrl(captchaId) - -const - SecureChars = {'A'..'Z', 'a'..'z', '0'..'9', '_', '\128'..'\255'} - -proc setError(c: var TForumData, field, msg: string): bool {.inline.} = - c.invalidField = field - c.errorMsg = "Error: " & msg - return false - -proc register(c: var TForumData, name, pass, antibot, email: string): bool = - # Username validation: - if name.len == 0 or not allCharsInSet(name, SecureChars): - return setError(c, "name", "Invalid username!") - if GetValue(db, sql"select name from person where name = ?", name).len > 0: - return setError(c, "name", "Username already exists!") - - # Password validation: - if pass.len < 4: - return setError(c, "new_password", "Invalid password!") - - # antibot validation: - let correctRes = GetValue(db, - sql"select answer from antibot where ip = ?", c.req.ip) - if antibot != correctRes: - return setError(c, "antibot", "You seem to be a bot!") - - # email validation - if not validEmailAddress(email): - return setError(c, "email", "Invalid email address") - - # perform registration: - var salt = makeSalt() - Exec(db, sql("INSERT INTO person(name, password, email, salt, status, lastOnline) " & - "VALUES (?, ?, ?, ?, 'user', DATETIME('now'))"), name, - makePassword(pass, salt), email, salt) - # return setError(c, "", "Could not create your account!") - return true - -proc checkLoggedIn(c: var TForumData) = - let pass = c.req.cookies["sid"] - if pass.len == 0: return - if ExecAffectedRows(db, - sql("update session set lastModified = DATETIME('now') " & - "where ip = ? and password = ?"), - c.req.ip, pass) > 0: - c.userpass = pass - c.userid = GetValue(db, - sql"select userid from session where ip = ? and password = ?", - c.req.ip, pass) - - let row = getRow(db, - sql"select name, email, admin from person where id = ?", c.userid) - c.username = ||row[0] - c.email = ||row[1] - c.isAdmin = parseBool(||row[2]) - # Update lastOnline - db.exec(sql"update person set lastOnline = DATETIME('now') where id = ?", - c.userid) - - else: - echo("SID not found in sessions. Assuming logged out.") - -proc logout(c: var TForumData) = - const query = sql"delete from session where ip = ? and password = ?" - c.username = "" - c.userpass = "" - Exec(db, query, c.req.ip, c.req.cookies["sid"]) - -proc incrementViews(c: var TForumData) = - const query = sql"update thread set views = views + 1 where id = ?" - Exec(db, query, $c.threadId) - -proc isPreview(c: TForumData): bool = - result = c.req.params["previewBtn"].len > 0 # TODO: Could be wrong? - -proc isDelete(c: TForumData): bool = - result = c.req.params["delete"].len > 0 - -proc rstToHtml(content: string): string = - result = rstgen.rstToHtml(content, {roSupportSmilies, roSupportMarkdown}, - docConfig) - -proc validateRst(c: var TForumData, content: string): bool = - result = true - try: - discard rstToHtml(content) - except EParseError: - result = setError(c, "", getCurrentExceptionMsg()) - -proc crud(c: TCrud, table: string, data: openArray[string]): TSqlQuery = - case c - of crCreate: - var fields = "insert into " & table & "(" - var vals = "" - for i, d in data: - if i > 0: - fields.add(", ") - vals.add(", ") - fields.add(d) - vals.add('?') - result = sql(fields & ") values (" & vals & ")") - of crRead: - var res = "select " - for i, d in data: - if i > 0: res.add(", ") - res.add(d) - result = sql(res & " from " & table) - of crUpdate: - var res = "update " & table & " set " - for i, d in data: - if i > 0: res.add(", ") - res.add(d) - res.add(" = ?") - result = sql(res & " where id = ?") - of crDelete: - result = sql("delete from " & table & " where id = ?") - -template retrSubject(c: expr) = - let subject = c.req.params["subject"] - if subject.len < 3: return setError(c, "subject", "Subject not long enough") - -template retrContent(c: expr) = - let content = c.req.params["content"] - if not validateRst(c, content): return false - -template retrPost(c: expr) = - retrSubject(c) - retrContent(c) - -template checkLogin(c: expr) = - if not loggedIn(c): return setError(c, "", "User is not logged in") - -template checkOwnership(c, postId: expr) = - if not c.isAdmin: - let x = getValue(db, sql"select author from post where id = ?", - postId) - if x != c.userId: - return setError(c, "", "You are not the owner of this post") - -template setPreviewData(c: expr) = - c.currentPost.subject = subject - c.currentPost.content = content - -template writeToDb(c, cr, postId: expr) = - exec(db, crud(cr, "post", "author", "ip", "header", "content", "thread"), - c.userId, c.req.ip, subject, content, $c.threadId, postId) - -proc edit(c: var TForumData, postId: int): bool = - checkLogin(c) - if c.isPreview: - retrPost(c) - setPreviewData(c) - elif c.isDelete: - checkOwnership(c, $postId) - if not TryExec(db, crud(crDelete, "post"), $postId): - return setError(c, "", "database error") - # delete corresponding thread: - if ExecAffectedRows(db, - sql"delete from thread where id not in (select thread from post)") > 0: - # whole thread has been deleted, so: - c.threadId = unselectedThread - result = true - else: - checkOwnership(c, $postId) - retrPost(c) - exec(db, crud(crUpdate, "post", "header", "content"), - subject, content, $postId) - result = true - -proc reply(c: var TForumData): bool = - checkLogin(c) - retrPost(c) - if c.isPreview: - setPreviewData(c) - else: - writeToDb(c, crCreate, "") - exec(db, sql"update thread set modified = DATETIME('now') where id = ?", - $c.threadId) - result = true - -proc newThread(c: var TForumData): bool = - const query = sql"insert into thread(name, views, modified) values (?, 0, DATETIME('now'))" - checkLogin(c) - retrPost(c) - if c.isPreview: - setPreviewData(c) - c.threadID = transientThread - else: - c.threadID = TryInsertID(db, query, c.req.params["subject"]).int - if c.threadID < 0: return setError(c, "subject", "Subject already exists") - writeToDb(c, crCreate, "") - result = true - -proc login(c: var TForumData, name, pass: string): bool = - # get form data: - const query = - sql"select id, name, password, email, salt, admin from person where name = ?" - if name.len == 0: - return c.setError("name", "Username cannot be nil.") - var success = false - for row in FastRows(db, query, name): - if row[2] == makePassword(pass, row[4]): - c.userid = row[0] - c.username = row[1] - c.userpass = row[2] - c.email = row[3] - c.isAdmin = row[5].parseBool - success = true - break - if success: - # create session: - Exec(db, - sql"insert into session (ip, password, userid) values (?, ?, ?)", - c.req.ip, c.userpass, c.userid) - return true - else: - return c.setError("password", "Login failed!") - -proc genActionMenu(c: var TForumData): string = - result = "" - var btns: seq[TStyledButton] = @[] - # TODO: Make this detection better? - if c.req.pathInfo notin ["/", "/login", "/register", "/dologin", "/doregister"]: - btns.add(("Thread List", c.req.makeUri("/", false))) - if c.loggedIn: - let hasReplyBtn = c.req.pathInfo != "/donewthread" and c.req.pathInfo != "/doreply" - if c.threadId >= 0 and hasReplyBtn: - let replyUrl = c.genThreadUrl("", "reply") & "#reply" - btns.add(("Reply", replyUrl)) - btns.add(("New Thread", c.req.makeUri("/newthread", false))) - result = c.genButtons(btns) - -proc getStats(c: var TForumData): TForumStats = - const totalUsersQuery = - sql"select count(*) from person" - result.totalUsers = getValue(db, totalUsersQuery).parseInt - const totalPostsQuery = - sql"select count(*) from post" - result.totalPosts = getValue(db, totalPostsQuery).parseInt - const totalThreadsQuery = - sql"select count(*) from thread" - result.totalThreads = getValue(db, totalThreadsQuery).parseInt - - var newestMemberCreation = 0 - result.activeUsers = @[] - const getUsersQuery = - sql"select id, name, admin, strftime('%s', lastOnline), strftime('%s', creation) from person" - for row in fastRows(db, getUsersQuery): - let secs = if row[3] == "": 0 else: row[3].parseint - let lastOnlineSeconds = getTime() - TTime(secs) - if lastOnlineSeconds < (60 * 5): # 5 minutes - result.activeUsers.add((row[1], row[0].parseInt, row[2].parseBool)) - if row[4].parseInt > newestMemberCreation: - result.newestMember = (row[1], row[0].parseInt, row[2].parseBool) - newestMemberCreation = row[4].parseInt - -include "forms.tmpl" -include "main.tmpl" - -proc prependRe(s: string): string = - result = if s.len == 0: - "" - elif s.startswith("Re:"): s - else: "Re: " & s - -template createTFD(): stmt = - var c: TForumData - init(c) - c.req = request - c.startTime = epochTime() - if request.cookies.len > 0: - checkLoggedIn(c) - -get "/": - createTFD() - resp genMain(c, genThreadsList(c), true) - -get "/t/@threadid/?": - createTFD() - parseInt(@"threadid", c.threadId, -1..1000_000) - if (@"postid").len > 0: - parseInt(@"postid", c.postId, -1..1000_000) - - if (@"action").len > 0: - case @"action" - of "reply": - let subject = GetValue(db, - sql"select header from post where id = (select max(id) from post where thread = ?)", - $c.threadId).prependRe - body = genPostsList(c, $c.threadId) - echo(c.threadId) - body.add genFormPost(c, "doreply", "Reply", subject, "", false) - of "edit": - cond c.postId != -1 - const query = sql"select header, content from post where id = ?" - let row = getRow(db, query, $c.postId) - let header = ||row[0] - let content = ||row[1] - body = genFormPost(c, "doedit", "Edit", header, content, true) - resp c.genMain(body) - else: - cond validThreadId(c) - incrementViews(c) - resp genMain(c, genPostsList(c, $c.threadId)) - -get "/login/?": - createTFD() - resp genMain(c, genFormLogin(c)) - -get "/logout/?": - createTFD() - logout(c) - redirect(uri("/")) - -get "/register/?": - createTFD() - resp genMain(c, genFormRegister(c)) - -template readIDs(): stmt = - # Retrieve the threadid and postid - if (@"threadid").len > 0: - parseInt(@"threadid", c.threadId, -1..1000_000) - if (@"postid").len > 0: - parseInt(@"postid", c.postId, -1..1000_000) - -template finishLogin(): stmt = - setCookie("sid", c.userpass, daysForward(7)) - redirect(uri("/")) - -template handleError(action: string, topText: string, isEdit: bool): stmt = - if c.isPreview: - body.add genPostPreview(c, @"subject", @"content", - c.userName, $getGMTime(getTime())) - body.add genFormPost(c, action, topText, reuseText, reuseText, isEdit) - resp genMain(c, body) - -post "/dologin": - createTFD() - if login(c, @"name", @"password"): - finishLogin() - else: - resp c.genMain(genFormLogin(c)) - -post "/doregister": - createTFD() - if c.register(@"name", @"new_password", @"antibot", @"email"): - discard c.login(@"name", @"new_password") - finishLogin() - else: - resp c.genMain(genFormRegister(c)) - -post "/donewthread": - createTFD() - if newThread(c): - redirect(uri("/")) - else: - body = "" - handleError("donewthread", "New thread", false) - -post "/doreply": - createTFD() - readIDs() - if reply(c): - redirect(c.genThreadUrl()) - else: - body = genPostsList(c, $c.threadId) - handleError("doreply", "Reply", false) - -post "/doedit": - createTFD() - readIDs() - if edit(c, c.postId): - redirect(c.genThreadUrl()) - else: - body = "" - handleError("doedit", "Edit", true) - -get "/newthread/?": - createTFD() - resp genMain(c, genFormPost(c, "donewthread", "New thread", "", "", false)) - -when isMainModule: - docConfig = rstgen.defaultConfig() - math.randomize() - db = Open(connection="nimforum.db", user="postgres", password="", - database="nimforum") - var http = true - if paramCount() > 0: - if paramStr(1) == "scgi": - http = false - run("", port = TPort(9000), http = http) - db.close() - diff --git a/license.txt b/license.txt new file mode 100644 index 0000000..3eb168b --- /dev/null +++ b/license.txt @@ -0,0 +1,18 @@ +Copyright (C) 2018 Andreas Rumpf, Dominik Picheta + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/localhost.local/public/css/custom-style.scss b/localhost.local/public/css/custom-style.scss new file mode 100644 index 0000000..6c9e50c --- /dev/null +++ b/localhost.local/public/css/custom-style.scss @@ -0,0 +1,16 @@ +// Use this to customise the styles of your forum. +$primary-color: #6577ac; +$body-font-color: #292929; +$dark-color: #505050; +$label-color: #7cd2ff; +$secondary-btn-color: #f1f1f1; + +// Define nav bar colours. +$body-bg: #ffffff; +$navbar-color: $body-bg; +$navbar-border-color-dark: $body-bg; +$navbar-primary-color: #e80080; + +#main-navbar input#search-box { + border: 1px solid #e6e6e6; +} \ No newline at end of file diff --git a/localhost.local/public/images/logo.png b/localhost.local/public/images/logo.png new file mode 100644 index 0000000..4ac52b7 Binary files /dev/null and b/localhost.local/public/images/logo.png differ diff --git a/main.tmpl b/main.tmpl deleted file mode 100644 index 964f1a0..0000000 --- a/main.tmpl +++ /dev/null @@ -1,53 +0,0 @@ -#! stdtmpl -#proc genMain(c: var TForumData, content: string, mainPage = false): string = -# result = "" - - - - Nimrod Forum - - - - -
-
- Homepage -
- - - -
- ${c.genActionMenu} -
- -
- $content - $c.errorMsg -
-
- ${c.genActionMenu} -
- - #if mainPage: - ${c.genListOnline} - #end if - -
- - - diff --git a/mockup/index.html b/mockup/index.html new file mode 100644 index 0000000..b836674 --- /dev/null +++ b/mockup/index.html @@ -0,0 +1,151 @@ + + + + + + + + + The Nim programming language forum + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TopicCategoryUsersRepliesViewsActivity
Few mixed up questions
help
+
+ +
+
+ +
+
+
+
+
+
+
+
554745m
Lexers and parsers in Nim
community
+
+
+
01444m
I need help 2
help
+
+ +
+
+ +
+
+
+
41d
+ + last visit + +
Nim v1.0 is here!
announcement
+
+ +
+
+ +
+
44d
+ + load more threads + +
+
+ + + + \ No newline at end of file diff --git a/mockup/thread.html b/mockup/thread.html new file mode 100644 index 0000000..298bb80 --- /dev/null +++ b/mockup/thread.html @@ -0,0 +1,232 @@ + + + + + + + + + The Nim programming language forum + + + + + + + + + +
+
+

Lexers and parsers in nim

+
community +
+
+
+
+
+ Avatar +
+
+
+
+
+ ErikCampobadal +
+
Jan 2015
+
+
+

Hey! I'm willing to create a programming language using nim.

+ +

It's an educational project. Been reading about compilers for weeks now and I started using tools like flex and bison for lexer and parser. I know nim have a parsing library but nowhere near that level.

+ +

There is an old post (2014) with a similar question so I'm bringing that back a few years later. Is there anything anyone know that could speed up the process of developing a programing language using nim? (I can have c code if needed ofc)

+
+
+ +
+ +
+
+ +
+
+
+
+ +
+
+
+ Avatar +
+
+
+
+
+ twetzel59 +
+
Jan 2015
+
+
+

Wow, I was just reading about the compilation pipeline today!

+ +

I suppose you could use at least the lexing part from a generator like flex, not so sure about using AST generators easily (it's possible).

+ +

Is your language complicated enough to warrant a parser generator or could you just use a custom parser?

+
+
+ +
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+ 3 years later +
+
+
+ +
+
+
+ Avatar +
+
+
+
+
+ dom96 +
+
32m
+
+
+

Let us test this new design a bit, shall we?

+
proc hello(x: int) =
+  echo("Hello ", x)
+
+42.hello()
Output
Hello 42
+ +

The greatest function ever written is hello.

+
+

Designing websites is often a pain.

+
Multi-level baby!
+

True that.

+

I also want to be able to support more detailed quoting:

+
+
+
+ Avatar +
+ Araq: + +
+ Unix is a cancer. +
+

We also want to be able to highlight user mentions:

+

Please let + + @Araq + + know that this forum is awesome.

+
+
+ +
+ +
+
+ +
+
+
+
+ + + +
+
+ +
+
+
+ Replying to "Lexers and parsers in nim" +
+
+ +
+
+
+ +
+
+ + + + \ No newline at end of file diff --git a/nimforum.nimble b/nimforum.nimble new file mode 100644 index 0000000..58a22f7 --- /dev/null +++ b/nimforum.nimble @@ -0,0 +1,64 @@ +# Package +version = "2.1.0" +author = "Dominik Picheta" +description = "The Nim forum" +license = "MIT" + +srcDir = "src" + +bin = @["forum"] + +skipExt = @["nim"] + +# Dependencies + +requires "nim >= 1.0.6" +requires "jester#405be2e" +requires "bcrypt#440c5676ff6" +requires "hmac#9c61ebe2fd134cf97" +requires "recaptcha#d06488e" +requires "sass#649e0701fa5c" + +requires "karax#5f21dcd" + +requires "webdriver#429933a" + +# Tasks + +task backend, "Compiles and runs the forum backend": + exec "nimble c src/forum.nim" + exec "./src/forum" + +task runbackend, "Runs the forum backend": + exec "./src/forum" + +task testbackend, "Runs the forum backend in test mode": + exec "nimble c -r -d:skipRateLimitCheck src/forum.nim" + +task frontend, "Builds the necessary JS frontend (with CSS)": + exec "nimble c -r src/buildcss" + exec "nimble js -d:release src/frontend/forum.nim" + mkDir "public/js" + cpFile "src/frontend/forum.js", "public/js/forum.js" + +task minify, "Minifies the JS using Google's closure compiler": + exec "closure-compiler public/js/forum.js --js_output_file public/js/forum.js.opt" + +task testdb, "Creates a test DB (with admin account!)": + exec "nimble c src/setup_nimforum" + exec "./src/setup_nimforum --test" + +task devdb, "Creates a test DB (with admin account!)": + exec "nimble c src/setup_nimforum" + exec "./src/setup_nimforum --dev" + +task blankdb, "Creates a blank DB": + exec "nimble c src/setup_nimforum" + exec "./src/setup_nimforum --blank" + +task test, "Runs tester": + exec "nimble c -y src/forum.nim" + exec "nimble c -y -r -d:actionDelayMs=0 tests/browsertester" + +task fasttest, "Runs tester without recompiling backend": + exec "nimble c -r -d:actionDelayMs=0 tests/browsertester" diff --git a/nimrod.cfg b/nimrod.cfg deleted file mode 100644 index 3ed332c..0000000 --- a/nimrod.cfg +++ /dev/null @@ -1,6 +0,0 @@ - -# we need the documentation generator of the compiler: ---path:"$nimrod/packages/docutils" - ---path:"$nimrod" ---path:"/home/dominik/code/nimrod/jester" \ No newline at end of file diff --git a/public/captchas/.gitignore b/public/captchas/.gitignore deleted file mode 100644 index d6b7ef3..0000000 --- a/public/captchas/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore diff --git a/public/css/nimforum.scss b/public/css/nimforum.scss new file mode 100644 index 0000000..2daecdb --- /dev/null +++ b/public/css/nimforum.scss @@ -0,0 +1,781 @@ +@import "custom-style"; + +// Import full Spectre source code +@import "spectre/src/spectre"; + +// Global styles. +// - TODO: Make these non-global. +.btn, .form-input { + margin-right: $control-padding-x; +} + +table th { + font-size: 0.65rem; +} + +// Spectre fixes. +// - Weird avatar outline. +.avatar { + background: transparent; +} + +// Custom styles. +// - Navigation bar. +$navbar-height: 60px; +$default-category-color: #a3a3a3; +$logo-height: $navbar-height - 20px; + +.navbar-button { + border-color: $navbar-border-color-dark; + background-color: $navbar-primary-color; + color: $navbar-color; + + &:focus { + box-shadow: none; + } + + &:hover { + background-color: darken($navbar-primary-color, 20%); + color: $navbar-color; + border-color: $navbar-border-color-dark; + } +} + +#main-navbar { + background-color: $navbar-color; + + .navbar { + height: $navbar-height; + } + + // Unfortunately we must colour the controls in the navbar manually. + .search-input { + @extend .form-input; + min-width: 120px; + border-color: $navbar-border-color-dark; + } + + .search-input:focus { + box-shadow: none; + border-color: $navbar-border-color-dark; + } + + .btn-primary { + @extend .navbar-button; + } + +} + +#img-logo { + vertical-align: middle; + height: $logo-height; +} + +.menu-right { + // To make sure the user menu doesn't move off the screen. + @media (max-width: 1600px) { + left: auto; + right: 0; + } + position: absolute; +} + +// - Main buttons +.btn-secondary { + background: $secondary-btn-color; + border-color: darken($secondary-btn-color, 5%); + color: invert($secondary-btn-color); + + margin-right: $control-padding-x*2; + + &:hover, &:focus { + background: darken($secondary-btn-color, 5%); + border-color: darken($secondary-btn-color, 10%); + + color: invert($secondary-btn-color); + } + + &:focus { + @include control-shadow(darken($secondary-btn-color, 40%)); + } +} + +#main-buttons { + margin-top: $control-padding-y*2; + margin-bottom: $control-padding-y*2; + + .dropdown > .btn { + @extend .btn-secondary; + } +} + +#category-selection { + .dropdown { + .btn { + margin-right: 0px; + } + } + .plus-btn { + margin-right: 0px; + i { + margin-right: 0px; + } + } +} + +.category-description { + opacity: 0.6; + font-size: small; +} + +.category-status { + font-size: small; + font-weight: bold; + + .topic-count { + margin-left: 5px; + opacity: 0.7; + font-size: small; + } +} + +.category { + white-space: nowrap; +} + +#new-thread { + .modal-container .modal-body { + max-height: none; + } + + .panel-body { + padding-top: $control-padding-y*2; + padding-bottom: $control-padding-y*2; + } + + .form-input[name='subject'] { + margin-bottom: $control-padding-y*2; + } + + textarea.form-input, .panel-body > div { + min-height: 40vh; + } + + .footer { + float: right; + margin-top: $control-padding-y*2; + } +} + +// - Thread table +.thread-title { + a, a:hover { + color: $body-font-color; + text-decoration: none; + } + + a.visited, a:visited { + color: lighten($body-font-color, 40%); + } + + i { + // Icon + margin-right: $control-padding-x-sm; + } +} + +.thread-list { + @extend .container; + @extend .grid-xl; +} + +.category-list { + @extend .thread-list; + + + .category-title { + @extend .thread-title; + a, a:hover { + color: lighten($body-font-color, 10%); + text-decoration: none; + } + } + + .category-description { + opacity: 0.6; + } +} + +#categories-list .category { + border-left: 6px solid; + border-left-color: $default-category-color; +} + +$super-popular-color: #f86713; +$popular-color: darken($super-popular-color, 25%); +$threads-meta-color: #545d70; + +.super-popular-text { + color: $super-popular-color; +} + +.popular-text { + color: $popular-color; +} + +.views-text { + color: $threads-meta-color; +} + +.label-custom { + color: white; + background-color: $label-color; + + font-size: 0.6rem; + padding-left: 0.3rem; + padding-right: 0.3rem; + border-radius: 5rem; +} + +.last-visit-separator { + td { + border-bottom: 1px solid $super-popular-color; + line-height: 0.1rem; + padding: 0; + text-align: center; + } + + span { + color: $super-popular-color; + padding: 0 8px; + font-size: 0.7rem; + background-color: $body-bg; + } +} + +.no-border { + td { + border: none; + } +} + +.category-color { + width: 0; + height: 0; + border: 0.25rem solid $default-category-color; + display: inline-block; + margin-right: 5px; +} + +.load-more-separator { + text-align: center; + color: darken($label-color, 35%); + background-color: lighten($label-color, 15%); + text-transform: uppercase; + font-weight: bold; + font-size: 80%; + cursor: pointer; + + td { + border: none; + padding: $control-padding-x $control-padding-y/2; + } +} + +// - Thread view +.title { + margin-top: $control-padding-y*2; + margin-bottom: $control-padding-y*2; + + p { + font-size: 1.4rem; + font-weight: bold; + + color: darken($dark-color, 20%); + + margin: 0; + } + + i.fas { + margin-right: $control-padding-x-sm; + } +} + +.thread-replies, .thread-time, .views-text, .popular-text, .centered-header { + text-align: center; +} + +.thread-users { + text-align: left; +} + +.thread-time { + color: $threads-meta-color; + + &.is-new { + @extend .text-success; + } + + &.is-old { + @extend .text-gray; + } + +} + +// Hide all the avatars but the first on small screens. +@media screen and (max-width: 600px) { + #threads-list a:not(:first-child) > .avatar { + display: none; + } +} + +.posts, .about { + @extend .grid-md; + @extend .container; + margin: 0; + padding: 0; + + margin-bottom: 10rem; // Just some empty space at the bottom. +} + +.post { + @extend .tile; + border-top: 1px solid $border-color; + padding-top: $control-padding-y-lg; + + &:target .post-main, &.highlight .post-main { + animation: highlight 2000ms ease-out; + } +} + +@keyframes highlight { + 0% { + background-color: lighten($primary-color, 20%); + } + 100% { + background-color: inherit; + } +} + +.post-icon { + @extend .tile-icon; +} + +.post-avatar { + @extend .avatar; + font-size: 1.6rem; + height: 2.5rem; + width: 2.5rem; +} + +.post-main { + @extend .tile-content; + + margin-bottom: $control-padding-y-lg*2; + // https://stackoverflow.com/a/41675912/492186 + flex: 1; + min-width: 0; +} + +.post-title { + margin-bottom: $control-padding-y*2; + + &, a, a:visited, a:hover { + color: lighten($body-font-color, 20%); + text-decoration: none; + } + + + .thread-title { + width: 100%; + + a > div { + display: inline-block; + } + } + + .post-username { + font-weight: bold; + display: inline-block; + + i { + margin-left: $control-padding-x; + } + } + + .post-metadata { + float: right; + + .post-replyingTo { + display: inline-block; + margin-right: $control-padding-x; + + i.fa-reply { + transform: rotate(180deg); + } + } + + .post-history { + display: inline-block; + margin-right: $control-padding-x; + + i { + font-size: 90%; + } + + .edit-count { + margin-right: $control-padding-x-sm/2; + } + } + } +} + +.post-content, .about { + img { + max-width: 100%; + } +} + +.post-buttons { + float: right; + + > div { + display: inline-block; + } + + .btn { + background: transparent; + border-color: transparent; + color: darken($secondary-btn-color, 40%); + + margin: 0; + margin-left: $control-padding-y-sm; + } + + .btn:hover { + background: $secondary-btn-color; + border-color: darken($secondary-btn-color, 5%); + color: invert($secondary-btn-color); + } + + .btn:focus { + @include control-shadow(darken($secondary-btn-color, 50%)); + } + + .btn:active { + box-shadow: inset 0 0 .4rem .01rem darken($secondary-btn-color, 80%); + } + + .like-button i:hover, .like-button i.fas { + color: #f783ac; + } + + .like-count { + margin-right: $control-padding-x-sm; + } +} + +#thread-buttons { + border-top: 1px solid $border-color; + width: 100%; + padding-top: $control-padding-y; + padding-bottom: $control-padding-y; + @extend .clearfix; + + .btn { + float: right; + margin-right: 0; + margin-left: $control-padding-x; + } +} + +blockquote { + border-left: 0.2rem solid darken($bg-color, 10%); + background-color: $bg-color; + + .detail { + margin-bottom: $control-padding-y; + color: lighten($body-font-color, 20%); + } +} + +.quote-avatar { + @extend .avatar; + @extend .avatar-sm; +} + +.quote-link { + float: right; +} + +.user-mention { + @extend .chip; + vertical-align: initial; + font-weight: bold; + display: inline-block; + font-size: 85%; + height: inherit; + padding: 0.08rem 0.4rem; + background-color: darken($bg-color-dark, 5%); + + img { + @extend .avatar; + @extend .avatar-sm; + } +} + +.code-buttons { + position: absolute; + bottom: 0; + right: 0; + + .btn-primary { + margin-bottom: $control-padding-y; + } +} + +.execution-result { + @extend .toast; + + h6 { + font-family: $base-font-family; + } +} + +.execution-success { + @extend .toast-success; +} + +.code { + // Don't show the "none". + &[data-lang="none"]::before { + content: ""; + } + + // &:not([data-lang="Nim"]) > .code-buttons { + // display: none; + // } +} +.code-buttons { + display: none; +} + +.post-content { + pre:not(.code) { + overflow: scroll; + } +} + +.information { + @extend .tile; + border-top: 1px solid $border-color; + padding-top: $control-padding-y-lg*2; + padding-bottom: $control-padding-y-lg*2; + color: lighten($body-font-color, 20%); + .information-title { + font-weight: bold; + } + + &.no-border { + border: none; + } +} + +.information-icon { + @extend .tile-icon; + + i { + width: $unit-16; + text-align: center; + font-size: 1rem; + + } +} + +.time-passed { + text-transform: uppercase; +} + +.load-more-posts { + text-align: center; + color: darken($label-color, 35%); + background-color: lighten($label-color, 15%); + border: none; + text-transform: uppercase; + font-weight: bold; + cursor: pointer; + + .information-main { + width: 100%; + text-align: left; + } + + .more-post-count { + color: rgba(darken($label-color, 35%), 0.5); + margin-right: $control-padding-x*2; + float: right; + } +} + +.form-input.post-text-area { + margin-top: $control-padding-y*2; + resize: vertical; +} + +#reply-box { + .panel { + margin-top: $control-padding-y*2; + } +} + +code { + color: $body-font-color; + background-color: $bg-color; +} + +tt { + @extend code; +} + +hr { + background: $border-color; + height: $border-width; + margin: $unit-2 0; + border: 0; +} + +.edit-box { + .edit-buttons { + margin-top: $control-padding-y*2; + + float: right; + + > div { + display: inline-block; + } + } + + .text-error { + margin-top: $control-padding-y*3; + display: inline-block; + } + + .form-input.post-text-area { + margin-bottom: $control-padding-y*2; + } +} + +@import "syntax.scss"; + +// - Profile view + +.profile { + @extend .tile; + margin-top: $control-padding-y*5; +} + +.profile-icon { + @extend .tile-icon; + margin-right: $control-padding-x; +} + +.profile-avatar { + @extend .avatar; + @extend .avatar-xl; + + height: 6.2rem; + width: 6.2rem; +} + +.profile-content { + @extend .tile-content; + padding: $control-padding-x $control-padding-y; +} + +.profile-title { + @extend .tile-title; +} + +.profile-stats { + dl { + border-top: 1px solid $border-color; + border-bottom: 1px solid $border-color; + padding: $control-padding-x $control-padding-y; + } + + dt { + font-weight: normal; + color: lighten($dark-color, 15%); + } + + dt, dd { + display: inline-block; + margin: 0; + margin-right: $control-padding-x; + } + + dd { + margin-right: $control-padding-x-lg; + } +} + +.profile-tabs { + margin-bottom: $control-padding-y-lg*2; +} + +.profile-post { + @extend .post; + + .profile-post-main { + flex: 1; + } + + .profile-post-time { + float: right; + } +} + +.spoiler { + text-shadow: gray 0px 0px 15px; + color: transparent; + -moz-user-select: none; + user-select: none; + cursor: normal; + + &:hover, &:focus { + text-shadow: $body-font-color 0px 0px 0px; + } +} + +.profile-post-title { + @extend .thread-title; +} + +// - Sign up modal + +#signup-modal { + .modal-container .modal-body { + max-height: 60vh; + } +} + +.license-text { + text-align: left; + font-size: 80%; +} + +// - Reset password +#resetpassword { + @extend .grid-sm; + @extend .container; + + .form-input { + display: inline-block; + width: 15rem; + margin-bottom: $control-padding-y*2; + } + + .footer { + margin-top: $control-padding-y*2; + } +} diff --git a/public/css/normalize.css b/public/css/normalize.css deleted file mode 100644 index 7dbb346..0000000 --- a/public/css/normalize.css +++ /dev/null @@ -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; } -} diff --git a/public/css/spectre b/public/css/spectre new file mode 160000 index 0000000..7a6af53 --- /dev/null +++ b/public/css/spectre @@ -0,0 +1 @@ +Subproject commit 7a6af53bca72b549b2cbf1948763348bd5b30dcd diff --git a/public/css/style.css b/public/css/style.css deleted file mode 100644 index 888f8f2..0000000 --- a/public/css/style.css +++ /dev/null @@ -1,308 +0,0 @@ -html, body { - height: 100%; -} - -#wrapper { - min-height: 100%; -} - -div#header { - background-color: #3d3d3d; - color: #ffffff; - padding-top: 3pt; - padding-bottom: 3pt; -} - -div#header span#welcome { - float: right; - padding: 0; - padding-right: 7pt; -} -div#header img { - float: right; - margin-top: -2pt; - padding-right: 7pt; -} - -div#header span, #nimbtn span { - margin: 0; - padding: 5pt; -} - -div#header a.right, #nimbtn a { - float: right; - - color: #ffffff; - margin-right: 6pt; -} - -div#header a { - color: #ffffff; -} - -div#header a:visited, #nimbtn a:visited { - color: #ffffff; -} - -div#header a:hover, #nimbtn a:hover { - text-decoration: none; -} - -#nimbtn a { - margin-left: 6pt; -} - -#nimbtn { - float: left; - background-color: #2a2a2a; - color: #ffffff; - padding-top: 3pt; - padding-bottom: 3pt; -} - -#content { - margin: 5pt; -} - -#content table#threads { - width: 100%; - border-collapse: separate; - text-align: center; - border: #ffffff solid 1px; - font-size: 10pt; -} - -#content table#threads th { - background-color: #5D5D5D; - background: -moz-linear-gradient(top, #5D5D5D, #4D4D4D); - background: -webkit-linear-gradient(top, #5D5D5D, #4d4d4d); - background: -o-linear-gradient(top, #5D5D5D, #4d4d4d); - color: #ffffff; - border-bottom: #2d2d2d solid 2px; - border-right: #2d2d2d solid 1px; -} - -#content table#threads tr:nth-child(even) { - background-color: #eee; -} - -#content table#threads td { - vertical-align: middle; - border-right: #9d9d9d solid 1px; - border-bottom: #9d9d9d solid 1px; -} - -#content table#threads>tbody>tr>td:first-child { - border-left: #9d9d9d solid 1px; - -} - -#content table#threads>tbody>tr>td:last-child { - border-right: #9d9d9d solid 1px; -} - -#content table#threads td:hover { - border-right-color: #9d9d9d; -} - -#content table#threads td.topic { - text-align: left; - padding: 5pt; -} -#content table#threads td.author { - width: 10%; -} -#content table#threads td.posts { - width: 10%; -} -#content table#threads td.views { - width: 10%; -} -#content table#threads td.lastreply { - width: 15%; -} - -#whoisonline { - margin: 5pt; - font-size: 9pt; -} - -#whoisonline .wioHeader { - background-color: #5D5D5D; - background: -moz-linear-gradient(top, #5D5D5D, #4D4D4D); - background: -webkit-linear-gradient(top, #5D5D5D, #4d4d4d); - background: -o-linear-gradient(top, #5D5D5D, #4d4d4d); - color: #ffffff; - border-bottom: #2d2d2d solid 1px; - padding: 3px; - padding-left: 5pt; -} - -#whoisonline .content { - border: #9d9d9d solid 1px; - border-top: #2D2D2D solid 1px; - padding: 5pt; -} - -#whoisonline .content hr { - margin-top: 5pt; - margin-bottom: 5pt; -} - -#footer { - background-color: #5D5D5D; - background: -moz-linear-gradient(top, #5D5D5D, #4D4D4D); - background: -webkit-linear-gradient(top, #5D5D5D, #4d4d4d); - background: -o-linear-gradient(top, #5D5D5D, #4d4d4d); - color: #ffffff; - - padding-left: 5px; - padding-right: 5px; -} - -#footer a:link, #footer a:visited { - color: #ffffff; -} - -#footer a:hover { - text-decoration: none; -} - -#topbar { - margin: 5pt; -} - -#content .post { - border: #4d4d4d solid 2px; - width: 100%; - margin-bottom: 5pt; -} - -#content .post th { - background-color: #5D5D5D; - background: -moz-linear-gradient(top, #5D5D5D, #4D4D4D); - background: -webkit-linear-gradient(top, #5D5D5D, #4d4d4d); - background: -o-linear-gradient(top, #5D5D5D, #4d4d4d); - color: #ffffff; - padding-left: 5pt; - padding-right: 5pt; - padding-top: 3pt; - padding-bottom: 3pt; - text-align: left; - font-size: 9pt; -} - -#content .post .left { - border-left: #4d4d4d solid 2px; - background-color: #eee; - padding: 7pt; - width: 15%; - height: auto; -} - -#content .post .left hr { - margin: 0; - margin-bottom: 5pt; - margin-top: 2pt; -} - -#content .post .content { - padding: 6pt; -} - -div#replywrapper { - width: 70%; - margin-left: auto; - margin-right: auto; - border: #4d4d4d solid 2px; -} - -div#replywrapper div#replytop { - background-color: #5D5D5D; - background: -moz-linear-gradient(top, #5D5D5D, #4D4D4D); - background: -webkit-linear-gradient(top, #5D5D5D, #4d4d4d); - background: -o-linear-gradient(top, #5D5D5D, #4d4d4d); - font-size: 9pt; - color: #ffffff; - padding: 5pt; - -} - -div#replywrapper form textarea { - width: 99%; -} - -div#replywrapper form > input:first-child { - width: 80%; -} - -div#replywrapper form { - padding: 8pt; -} - -/* For RST nimrod syntax highlighter */ -span.DecNumber {color: blue} -span.BinNumber {color: blue} -span.HexNumber {color: blue} -span.OctNumber {color: blue} -span.FloatNumber {color: blue} -span.Identifier {color: black} -span.Keyword {font-weight: bold} -span.StringLit {color: blue} -span.LongStringLit {color: blue} -span.CharLit {color: blue} -span.EscapeSequence {color: black} -span.Operator {color: black} -span.Punctation {color: black} -span.Comment, span.LongComment {font-style:italic; color: green} -span.RegularExpression {color: DarkViolet} -span.TagStart {color: DarkViolet} -span.TagEnd {color: DarkViolet} -span.Key {color: blue} -span.Value {color: black} -span.RawData {color: blue} -span.Assembler {color: blue} -span.Preprocessor {color: DarkViolet} -span.Directive {color: DarkViolet} -span.Command, span.Rule, span.Hyperlink, span.Label, span.Reference, -span.Other {color: black} - -/* Buttons */ -a.button { - border-radius: 2px 2px 2px 2px; - background: -moz-linear-gradient(top, #f7f7f7, #ebebeb); - background: -webkit-linear-gradient(top, #f7f7f7, #ebebeb); - background: -o-linear-gradient(top, #f7f7f7, #ebebeb); - text-decoration: none; - color: #3d3d3d; - padding: 5px; - border: solid 1px #9d9d9d; - display: inline-block; - position: relative; - text-align: center; - font-size: small; -} - -a.button.left { - border-top-right-radius: 0; - border-bottom-right-radius: 0; -} - -a.button.middle { - border-radius: 0; - border-left: 0; -} - -a.button.right { - border-top-left-radius: 0; - border-bottom-left-radius: 0; - border-left: 0; -} - -a.button:hover { - background: -moz-linear-gradient(top, #0099c7, #0294C1); - background: -webkit-linear-gradient(top, #0099c7, #0294C1); - background: -o-linear-gradient(top, #0099c7, #0294C1); - border: solid 1px #077A9C; - color: #ffffff; -} \ No newline at end of file diff --git a/public/css/syntax.scss b/public/css/syntax.scss new file mode 100644 index 0000000..14dfa49 --- /dev/null +++ b/public/css/syntax.scss @@ -0,0 +1,13 @@ +pre .Comment { color:#618f0b; font-style:italic; } +pre .Keyword { color:rgb(39, 141, 182); font-weight:bold; } +pre .Type { color:#128B7D; font-weight:bold; } +pre .Operator { font-weight: bold; } +pre .atr { color:#128B7D; font-weight:bold; font-style:italic; } +pre .def { color:#CAD6E4; font-weight:bold; font-style:italic; } +pre .StringLit { color:rgb(190, 15, 15); font-weight:bold; } +pre .DecNumber, pre .FloatNumber { color:#8AB647; } +pre .tab { border-left:1px dotted rgba(67,168,207,0.4); } +pre .EscapeSequence +{ + color: #C08D12; +} \ No newline at end of file diff --git a/public/karax.html b/public/karax.html new file mode 100644 index 0000000..3f7f41b --- /dev/null +++ b/public/karax.html @@ -0,0 +1,31 @@ + + + + + + + + + $title + + + + + + + + + + + +
+ + + + diff --git a/public/license.rst b/public/license.rst new file mode 100644 index 0000000..beebae3 --- /dev/null +++ b/public/license.rst @@ -0,0 +1,38 @@ +Content license +=============== + +All the content contributed to $hostname is `cc-wiki (aka cc-by-sa) +`_ licensed, intended to be +**shared and remixed**. + +The cc-wiki licensing, while intentionally permissive, does require +attribution: + +**Attribution** — You must attribute the work in the manner specified by +the author or licensor (but not in any way that suggests that they endorse +you or your use of the work). + +This means that if you republish this content, you are +required to: + +* **Visually indicate that the content is from the $name**. It doesn’t + have to be obnoxious; a discreet text blurb is fine. +* **Hyperlink directly to the original post** (e.g., + https://$hostname/t/186/1#908) +* **Show the author names** for every post. +* **Hyperlink each author name** directly back to their user profile page + (e.g., http://$hostname/profile/Araq) + +To be more specific, each hyperlink must +point directly to the $hostname domain in +standard HTML visible even with JavaScript disabled, and not use a tinyurl or +any other form of obfuscation or redirection. Furthermore, the links must not +be `nofollowed +`_. + +This is about the spirit of fair **attribution**. Attribution to the website, +and more importantly, to the individuals who so generously contributed their +time to create that content in the first place! + +Feel free to remix and reuse to your heart’s content, as long as a good faith +effort is made to attribute the content! diff --git a/rst.txt b/public/rst.rst similarity index 62% rename from rst.txt rename to public/rst.rst index 15813a5..b5db812 100644 --- a/rst.txt +++ b/public/rst.rst @@ -1,9 +1,8 @@ -=========================================================================== - reStructuredText cheat sheet -=========================================================================== +Markdown and RST supported by this forum +======================================== This is a cheat sheet for the *reStructuredText* dialect as implemented by -Nimrod's documentation generator which has been reused for this forum. :-) +Nim's documentation generator which has been reused for this forum. See also the `official RST cheat sheet `_ @@ -12,9 +11,8 @@ for further information. Elements of **markdown** are also supported. - Inline elements -=============== +--------------- Ordinary text may contain *inline elements*: @@ -29,68 +27,71 @@ Plain text Result ``\\escape`` \\escape =============================== ============================================ -Links -===== +Quoting other users can be done by prefixing their message with ``>``:: -Links are either direct URLs like ``http://nimrod-code.org`` or written like + > Hello World + + Hi! + +Which will result in: + +> Hello World + +Hi! + +Links +----- + +Links are either direct URLs like ``https://nim-lang.org`` or written like this:: - `Nimrod `_ + `Nim `_ Or like this:: - ``_ + ``_ Code blocks -=========== +----------- -are done this way:: +The code blocks can be written in the same style as most common Markdown +flavours:: - .. code-block:: nimrod + ```nim + if x == "abc": + echo "xyz" + ``` + +or using RST syntax:: + + .. code-block:: nim if x == "abc": echo "xyz" +Both are rendered as: -Is rendered as: - -.. code-block:: nimrod - - if x == "abc": - echo "xyz" - - -Except Nimrod, the programming languages C, C++, Java and C# have highlighting -support. - -An alternative github-like syntax is also supported. This has the advantage -that no excessive indentation is needed:: - - ```nimrod - if x == "abc": - echo "xyz"``` - -Is rendered as: - -.. code-block:: nimrod +.. code-block:: nim if x == "abc": echo "xyz" +Apart from Nim, the programming languages C, C++, Java and C# also +have highlighting support. Literal blocks -============== +-------------- -Are introduced by '::' and a newline. The block is indicated by indentation: +These are introduced by '::' and a newline. The block is indicated by indentation: :: :: if x == "abc": echo "xyz" -Is rendered as:: +The above is rendered as:: if x == "abc": echo "xyz" @@ -98,9 +99,9 @@ Is rendered as:: Bullet lists -============ +------------ -look like this:: +Bullet lists look like this:: * Item 1 * Item 2 that @@ -111,7 +112,7 @@ look like this:: - item 3b - valid bullet characters are ``+``, ``*`` and ``-`` -Is rendered as: +The above rendered as: * Item 1 * Item 2 that spans over multiple lines @@ -123,9 +124,9 @@ Is rendered as: Enumerated lists -================ +---------------- -are written like this:: +Enumerated lists are written like this:: 1. This is the first item 2. This is the second item @@ -133,64 +134,17 @@ are written like this:: single letters, or roman numerals #. This item is auto-enumerated -Is rendered as: +They are rendered as: 1. This is the first item 2. This is the second item 3. Enumerators are arabic numbers, single letters, or roman numerals -#. This item is auto-enumerated - - -Quoting someone -=============== - -quotes are just:: - - **Someone said**: Indented paragraphs, - - and they may nest. - -Is rendered as: - - **Someone said**: Indented paragraphs, - - and they may nest. - - - -Definition lists -================ - -are written like this:: - - what - Definition lists associate a term with - a definition. - - how - The term is a one-line phrase, and the - definition is one or more paragraphs or - body elements, indented relative to the - term. Blank lines are not allowed - between term and definition. - -and look like: - -what - Definition lists associate a term with - a definition. - -how - The term is a one-line phrase, and the - definition is one or more paragraphs or - body elements, indented relative to the - term. Blank lines are not allowed - between term and definition. +#. This item is auto-enumerated Tables -====== +------ Only *simple tables* are supported. They are of the form:: @@ -218,3 +172,39 @@ Cell 4 Cell 5; any Cell 6 multiple lines Cell 7 Cell 8 Cell 9 ================== =============== =================== + +Images +------ + +Image embedding is supported. This includes GIFs as well as mp4 (for which a +