42 |
43 |
67 |
68 |
69 |
--------------------------------------------------------------------------------
/meta/data/post/0/0/3.txt:
--------------------------------------------------------------------------------
1 | Date: 2012-03-25 00:00:00 +0000
2 | Title: About MathB.in
3 | Name:
4 |
5 | [MathB.in] is a website meant for sharing snippets
6 | of mathematical text with others on the web. This
7 | is a pastebin for mathematics. The source code of
8 | MathB.in is available at
9 | .
10 |
11 | [MathB.in]: ./
12 |
13 |
14 | Posting and Sharing
15 | -------------------
16 |
17 | A new post can be composed by visiting the [home
18 | page](/) and writing or pasting code in the box on
19 | the left hand pane of the page. Once a post is
20 | composed and submitted, the page is saved and it
21 | becomes accessible with a new unique URL. The new
22 | page looks similar to this page and it has a
23 | unique URL of its own. The URL can be shared with
24 | anyone on the web and they will be able to visit
25 | your post.
26 |
27 |
28 | Post Format
29 | -----------
30 |
31 | A post can be composed in a mixture of LaTeX and
32 | Markdown text. For a demonstration of how LaTeX
33 | and Markdown is rendered, see the [demo page](1).
34 | To quickly get started with posting mathematical
35 | snippets, see the [tutorial](2).
36 |
37 |
38 | Usage
39 | -----
40 |
41 | This website is meant for creating mathematical
42 | snippets. Science and technical posts are also
43 | welcome on this website. Any other type of post
44 | should not be posted to this website. Any post
45 | that is not mathematical, scientific, or technical
46 | in nature will be removed.
47 |
48 |
49 | History
50 | -------
51 |
52 | [MathB.in] is the oldest mathematics pastebin that
53 | is still online and serving its community of
54 | users. It isn't the first mathematics pastebin
55 | though. It's the second. The first pastebin was
56 | written by Mark A. Stratman. It was hosted at the
57 | domain *mathbin.net* until 2020.
58 |
59 | MathB.in was born on Sunday, 25 March 2012, after
60 | a single night of furious coding. This was a
61 | result of stumbling upon [math.stackexchange.com]
62 | the previous night which used MathJax to render
63 | mathematics formula on the web browser. Thanks to
64 | that chance encounter with MathJax, the rest of
65 | the Saturday night was spent in coding a new
66 | mathematics pastebin using MathJax and PHP. After
67 | coding all through the night, registering a new
68 | domain name, and setting up a website, [MathB.in]
69 | was released early Sunday morning.
70 |
71 | The current version of MathB.in no longer runs on
72 | PHP. It has been rewritten in Common Lisp since
73 | then. See the blog post [MathB.in Turns Ten] for
74 | more details about the history of MathB.in.
75 |
76 | [math.stackexchange.com]: https://math.stackexchange.com/
77 | [MathB.in Turns Ten]: https://susam.net/blog/mathbin-turns-ten.html
78 |
79 |
80 | Software
81 | --------
82 |
83 | MathB.in is powered by the following software:
84 |
85 | - [SBCL](https://www.sbcl.org/)
86 | - [Hunchentoot](https://edicl.github.io/hunchentoot/)
87 | - [MathB](https://github.com/susam/texme)
88 | - [TeXMe](https://github.com/susam/texme)
89 | - [MathJax](https://www.mathjax.org/)
90 | - [Marked](https://github.com/markedjs/marked)
91 | - [Nginx](https://www.nginx.com/)
92 | - [Debian GNU/Linux](https://www.debian.org/)
93 |
94 |
95 | Bug Reports and Suggestions
96 | ---------------------------
97 |
98 | To report bugs, suggest improvements, or ask
99 | questions, create a new issue at
100 | .
101 |
--------------------------------------------------------------------------------
/meta/data/post/0/0/1.txt:
--------------------------------------------------------------------------------
1 | Date: 2012-03-25 00:00:00 +0000
2 | Title: MathB.in Demo
3 | Name:
4 |
5 | Binomial Theorem
6 | ----------------
7 |
8 | $$ (x+y)^n = \sum_{k=0}^n {n \choose k} x^{n - k} y^k. $$
9 |
10 |
11 | Exponential Function
12 | --------------------
13 |
14 | $$ e^x = \lim_{n \to \infty} \left( 1+ \frac{x}{n} \right)^n. $$
15 |
16 |
17 | Cauchy-Schwarz Inequality
18 | -------------------------
19 |
20 | $$
21 | \left( \sum_{k=1}^n a_k b_k \right)^2 \leq
22 | \left( \sum_{k=1}^n a_k^2 \right)
23 | \left( \sum_{k=1}^n b_k^2 \right).
24 | $$
25 |
26 |
27 | Bayes' Theorem
28 | --------------
29 |
30 | $$ P(A \mid B) = \frac{P(B \mid A) \, P(A)}{P(B)}. $$
31 |
32 |
33 | Euler's Summation Formula
34 | -------------------------
35 |
36 | **Theorem** (Euler's summation formula). _If $ f $
37 | has a continuous derivative $ f' $ on the interval
38 | $ [y, x], $ where $ 0 < y < x, $ then_
39 | \begin{align}
40 | \sum_{y < n \le x} f(n)
41 | = & \int_y^x f(t) \, dt + \int_y^x (t - [t]) f'(t) \, dt
42 | \notag \\
43 | & + f(x)([x] - x) - f(y)([y] - y). \label{theorem}
44 | \end{align}
45 |
46 | _Proof._ Let $ m = [y] $ and $ k = [x]. $ For
47 | integers $ n $ and $ n - 1 $ in $ [y, x] $ we have
48 | \begin{align*}
49 | \int_{n - 1}^n [t] f'(t) \, dt
50 | & = \int_{n - 1}^n (n - 1) f'(t) \, dt \\
51 | & = (n - 1) \{ f(n) - f(n - 1) \} \\
52 | & = \{n f(n) - (n - 1) f(n - 1)\} - f(n).
53 | \end{align*}
54 | Summing from $ n = m + 2 $ to $ n = k $ we find
55 | that the first sum telescopes, hence
56 | \begin{align*}
57 | \int_{m + 1}^k [t] f'(t) \, dt
58 | & = k f(k) - (m + 1) f(m + 1) - \sum_{n = m + 2}^k f(n) \\
59 | & = k f(k) - m f(m + 1) - \sum_{y < n \le x} f(n).
60 | \end{align*}
61 | Therefore
62 | \begin{align}
63 | \sum_{y < n \le x} f(n)
64 | & = - \int_{m + 1}^k [t] f'(t) \, dt + k f(k) - m f(m + 1) \notag \\
65 | & = - \int_y^x [t] f'(t) \, dt + k f(x) - m f(y). \label{summation}
66 | \end{align}
67 | Integration by parts gives us
68 | $$
69 | \int_y^x f(t) \, dt = x f(x) - y f(y) - \int_y^x t f'(t) \, dt,
70 | $$
71 | and when this is combined with \eqref{summation}
72 | we obtain \eqref{theorem}.
73 |
74 |
75 | Hello World Program
76 | -------------------
77 |
78 | Here is an example of `"hello, world"` program
79 | written in the C programming language:
80 |
81 | ```
82 | #include
83 |
84 | int main()
85 | {
86 | printf("hello, world\n");
87 | return 0;
88 | }
89 | ```
90 |
91 |
92 | Issac Newton Quotes
93 | -------------------
94 |
95 | Issac Newton was relatively modest about his
96 | achievements, writing in a letter to Robert Hooke
97 | in February 1676:
98 |
99 | > If I have seen further it is by standing on the
100 | shoulders of giants.
101 |
102 | In a later memoir, Newton wrote:
103 |
104 | > I do not know what I may appear to the world,
105 | > but to myself I seem to have been only like a
106 | > boy playing on the sea-shore, and diverting
107 | > myself in now and then finding a smoother pebble
108 | > or a prettier shell than ordinary, whilst the
109 | > great ocean of truth lay all undiscovered before
110 | > me.
111 |
112 |
113 | Table of Number Theory Functions
114 | --------------------------------
115 |
116 | The following table shows information about a few
117 | important functions in number theory.
118 |
119 | | Name | Notation | First few values | Multiplicative property |
120 | |-|-|-|-|
121 | | Möbius function | $ \mu(n) $ | $ 1, -1, -1, 0, -1 $ | Multiplicative |
122 | | Euler's totient function | $ \varphi(n) $ | $ 1, 1, 2, 2, 4 $ | Multiplicative |
123 | | Mangoldt function | $ \Lambda(n) $ | $ 0, \log 2, \log 3, \log 2, \log 5 $ | Not multiplicative |
124 | | Liouville's function | $ \lambda(n) $ | $ 1, -1, -1, 1, -1 $ | Completely multiplicative |
125 |
126 |
127 | About This Demo
128 | ---------------
129 |
130 | This is a demo of a MathB.in post written in
131 | Markdown + LaTeX.
132 |
--------------------------------------------------------------------------------
/meta/data/post/0/0/4.txt:
--------------------------------------------------------------------------------
1 | Date: 2012-09-20 00:00:00 +0000
2 | Title: Privacy Notice
3 | Name:
4 |
5 | [MathB.in] (this website) is a community website
6 | maintained by one or more volunteers. This
7 | privacy notice explains how this website uses the
8 | data it collects from you when you use this
9 | website.
10 |
11 | [MathB.in]: ./
12 |
13 |
14 | ## Data Items
15 |
16 | This website collects the following data:
17 |
18 | - The text submitted by you while submitting a new
19 | post using this website. This text may contain
20 | a post title, your name, and arbitrary code
21 | written in LaTeX and Markdown format. This text
22 | is written by you, so you are in complete
23 | control of what you submit in the text. You
24 | must not submit sensitive personal data to this
25 | website.
26 |
27 | - Your Internet Protocol (IP) address used to
28 | visit this website.
29 |
30 | - HTTP referrer URL sent by your web browser or
31 | client software while connecting to this
32 | website. A web browser or a client software
33 | typically, but not necessarily, sends the URL of
34 | the web page or website where you clicked on a
35 | link to visit this website as the HTTP referrer
36 | value. If this website is visited directly
37 | without clicking on any link, then a web browser
38 | or client does not typically send any HTTP
39 | referrer data.
40 |
41 | - User agent string sent by your web browser or
42 | client software while visiting this website. A
43 | web browser or a client software typically, but
44 | not necessarily, sends the names and versions of
45 | your device, operating system, and web browser
46 | or client software as the user agent string.
47 |
48 |
49 | ## Data Collection
50 |
51 | When you visit this website, your IP address,
52 | referrer data, and user agent string are
53 | automatically recorded in web server log files.
54 |
55 | When you submit a new post to this website
56 | successfully, your text is written to a file on
57 | the web server and your text is published at a new
58 | URL that is shown to you immediately after you
59 | successfully submit the post. If the post
60 | submission fails due to errors, then your text is
61 | neither saved nor published.
62 |
63 |
64 | ## Data Usage
65 |
66 | The IP address, referrer data, and user agent
67 | string is used to monitor abuse of this website
68 | such as submitting spam posts, submitting
69 | non-mathematical posts, sending too many requests
70 | to reduce the availability of this website, etc.
71 | This data is also used to mitigate abuse of this
72 | website by blocking certain combinations of IP
73 | address, referrer data, and/or user agent from
74 | accessing this website or by limiting the number
75 | of requests such a combination may send to this
76 | website.
77 |
78 | The post text is saved in a text file for an
79 | indefinitely long period of time. The post text
80 | is published at a new URL. This post text is
81 | available publicly to anyone who visits this URL.
82 | This URL is shown to you immediately after you
83 | successfully submit a new post. You must not
84 | submit sensitive personal data to this website.
85 |
86 |
87 | ## Data Storage
88 |
89 | The IP address, referrer data, and user agent
90 | string are stored in web server log files. This
91 | data is retained for 15 days. After 15 days, this
92 | data is deleted automatically.
93 |
94 | The post text is saved in text files and made
95 | available publicly on this website via unique
96 | URLs. Anyone with the URL of your post can view
97 | the post text. You must not submit sensitive
98 | personal data to this website.
99 |
100 |
101 | ## Cookies
102 |
103 | This website does not use cookies.
104 |
105 |
106 | ## Third-Party Analytics
107 |
108 | This website does not use third-party analytics.
109 |
110 |
111 | ## Marketing
112 |
113 | The data collected from you is not used for marketing.
114 |
115 |
116 | ## Contact
117 |
118 | If you have any questions about the data collected
119 | by this website, please create a new issue at
120 | [github.com/susam/mathb/issues][issues] or contact
121 | susam (dot) pal (at) gmail (dot) com.
122 |
123 | [issues]: https://github.com/susam/mathb/issues
124 |
--------------------------------------------------------------------------------
/web/css/mathb.css:
--------------------------------------------------------------------------------
1 | body {
2 | color: #333;
3 | font-family: georgia, serif;
4 | line-height: 1.5;
5 | margin: 0 auto;
6 | padding: 0 1em;
7 | }
8 | a:link {
9 | color: #00e;
10 | }
11 | a:visited {
12 | color: #518;
13 | }
14 | a:focus, a:hover {
15 | color: #03f;
16 | }
17 | a:active {
18 | color: #e00;
19 | }
20 | header h1 {
21 | text-align: center;
22 | margin: 0.5em 0;
23 | }
24 | header h1 a:link, header h1 a:visited {
25 | color: #333;
26 | text-decoration: none;
27 | }
28 | header h1 a:focus, header h1 a:hover {
29 | color: #555;
30 | text-decoration: underline;
31 | }
32 | header nav {
33 | display: none;
34 | }
35 | footer hr {
36 | border: none;
37 | border-bottom: thin dotted #666;
38 | margin: 1em 0;
39 | }
40 | form {
41 | background: #eee;
42 | border: thin solid #ccc;
43 | padding: 1em;
44 | }
45 | textarea {
46 | box-sizing: border-box;
47 | display: block;
48 | width: 100%;
49 | max-width: 100%;
50 | padding: 0.5rem;
51 | min-height: 10rem;
52 | }
53 | #view {
54 | box-sizing: border-box;
55 | min-height: calc(100vh - 38em);
56 | }
57 | input {
58 | box-sizing: border-box;
59 | display: block;
60 | width: 100%;
61 | padding: 0.5rem;
62 | margin-top: 1rem;
63 | }
64 | #submit, #copy {
65 | box-sizing: border-box;
66 | display: block;
67 | width: 50%;
68 | margin-left: auto;
69 | margin-right: auto;
70 | }
71 | #output {
72 | margin-top: 1em;
73 | }
74 | #sheet {
75 | border: thin solid #999;
76 | overflow: auto;
77 | }
78 | #output {
79 | box-sizing: border-box;
80 | }
81 | #copy {
82 | width: 25%;
83 | }
84 | footer div {
85 | max-width: 40em;
86 | margin-left: auto;
87 | margin-right: auto;
88 | }
89 | footer nav, footer nav + p {
90 | text-align: center;
91 | }
92 | footer nav a {
93 | line-height: 2.5em;
94 | }
95 | header nav a, footer nav a {
96 | margin-right: 2em;
97 | }
98 | header nav a:last-child, footer nav a:last-child {
99 | margin-right: 0;
100 | }
101 | #view {
102 | padding: 1em;
103 | }
104 | #date {
105 | font-size: small;
106 | color: #666;
107 | line-height: 1.5rem;
108 | text-align: right;
109 | padding: 0 1rem;
110 | }
111 | noscript, #error {
112 | font-size: large;
113 | color: #f00;
114 | border-color: #f00;
115 | }
116 | #error {
117 | margin-bottom: 1em;
118 | }
119 | #widget {
120 | display: none;
121 | }
122 | .post #widget {
123 | display: block;
124 | }
125 | .notice {
126 | border: thick double #f00;
127 | }
128 | @media screen and (min-width: 50em) {
129 | header h1 {
130 | display: inline-block;
131 | width: 50%;
132 | text-align: left;
133 | }
134 | header nav {
135 | display: inline-block;
136 | width: 50%;
137 | text-align: right;
138 | }
139 | form, #output {
140 | display: inline-block;
141 | box-sizing: border-box;
142 | width: calc(50% - 0.5em);
143 | vertical-align: top;
144 | }
145 | textarea {
146 | min-height: calc(100vh - 19rem);
147 | }
148 | #output {
149 | margin-left: 1em;
150 | margin-top: 0em;
151 | }
152 | #url {
153 | display: inline-block;
154 | width: 75%;
155 | }
156 | #copy {
157 | display: inline-block;
158 | margin-left: 1em;
159 | width: calc(25% - 1em);
160 | }
161 | #view {
162 | min-height: calc(100vh - 12em);
163 | }
164 | }
165 | @media (prefers-color-scheme: dark) {
166 | body {
167 | background: #111;
168 | color: #bbb;
169 | }
170 | a:link {
171 | color: #9bf;
172 | }
173 | a:visited {
174 | color: #caf;
175 | }
176 | a:focus, a:hover {
177 | color: #9cf;
178 | }
179 | a:active {
180 | color: #faa;
181 | }
182 | header h1 a:link, header h1 a:visited {
183 | color: #bbb;
184 | }
185 | header h1 a:focus, header h1 a:hover {
186 | color: #ddd;
187 | }
188 | form {
189 | background: #222;
190 | }
191 | input, textarea, button {
192 | background: #000;
193 | color: #bbb;
194 | }
195 | form, input, textarea, button, #output {
196 | border-color: #666;
197 | }
198 | noscript, #error {
199 | color: #f99;
200 | border-color: #f99;
201 | }
202 | .notice {
203 | font-size: large;
204 | border-color: #f99;
205 | margin-bottom: 1em;
206 | padding: 0 1em;
207 | }
208 | .notice p {
209 | max-width: 40em;
210 | margin-left: auto;
211 | margin-right: auto;
212 | }
213 | }
214 | @media print {
215 | header, form, footer, #date, .post #widget, .notice {
216 | display: none;
217 | }
218 | #sheet {
219 | border: 0;
220 | }
221 | }
222 |
--------------------------------------------------------------------------------
/CHANGES.md:
--------------------------------------------------------------------------------
1 | Changelog
2 | =========
3 |
4 | 1.3.0 (2025-03-16)
5 | ------------------
6 |
7 | This release captures the state of the MathB project as it was on Sun,
8 | 16 March 2025, when the `MathB.in` service powered by this software
9 | was shut down.
10 |
11 |
12 | ### Added
13 |
14 | - Runtime property `:expect` to enforce presence of tokens in code.
15 | - Special variable `*log-directory*` to specify directory to write
16 | logs to.
17 |
18 |
19 | ### Changed
20 |
21 | - Logs are now written to `/opt/log/mathb/` by default.
22 | - Change colour to display code from blue to green.
23 |
24 |
25 | ### Removed
26 |
27 | - Runtime property `:initial-year`.
28 | - Runtime property `:copyright-owner`.
29 | - Copyright notice from the footer.
30 |
31 |
32 | ### Fixed
33 |
34 | - Warning for `redefining HUNCHENTOOT:ACCEPTOR-STATUS-MESSAGE` during
35 | start-up.
36 |
37 |
38 | 1.2.0 (2022-10-16)
39 | ------------------
40 |
41 | ### Added
42 |
43 | - Runtime property `:lock-down` to disable post viewing and post
44 | submission.
45 | - Runtime property `:min-title-length` to specify minimum title
46 | length.
47 | - Runtime property `:min-name-length` to specify minimum name length.
48 | - Runtime property `:min-code-length` to specify minimum code length.
49 | - Runtime property `:initial-year` to customize the initial yearin
50 | footer.
51 | - Runtime property `:copyright-owner` to customize the owner name in
52 | footer.
53 | - Link to privacy notice.
54 | - Show only rendered content in print preview mode.
55 |
56 |
57 | ### Changed
58 |
59 | - Do not allow code textarea to be dragged to overlap with output
60 | sheet.
61 | - Reject post if title field contains carriage return or line feed
62 | character.
63 | - Reject post if name field contains carriage return or line feed
64 | character.
65 | - Remove carriage returns from code before writing to data file.
66 |
67 |
68 | ### Fixed
69 |
70 | - Rendering error for LaTeX commands that need additional MathJax
71 | extensions.
72 | - Alternate HTML elements in the output not getting sanitized.
73 |
74 |
75 | 1.1.0 (2022-09-30)
76 | ------------------
77 |
78 | ### Added
79 |
80 | - Runtime property `:ban` to reject posts from specific IP addresses.
81 | - Runtime property `:protect` to protect posts in case of data
82 | corruption.
83 | - Nginx configuration to work around Hunchentoot memory leakage issue.
84 | - Show metadata at URL path `/0`.
85 |
86 |
87 | ### Changed
88 |
89 | - Empty header value in post text file no longer has a trailing space.
90 |
91 |
92 | ### Fixed
93 |
94 | - Table rows disappearing from rendered output.
95 | - Incorrect zero top margin for display math.
96 | - HTTP 500 error on post submission when options file is missing.
97 |
98 |
99 | 1.0.0 (2022-09-20)
100 | ------------------
101 |
102 | ### Added
103 |
104 | - The first major release of MathB since 2012.
105 | - Common Lisp source code is made available in this release.
106 | - Dark colour scheme for clients that prefer dark colour scheme.
107 | - Responsive layout to adapt the user interface to narrow screens.
108 | - Use TeXMe to render Markdown + LaTeX.
109 | - Add post interval features to control flooding.
110 | - Data is read from `/opt/data/mathb/` by default.
111 | - Special variable `*data-directory*` to specify default data
112 | directory.
113 | - Control runtime behaviour with `opt.lisp`.
114 | - Runtime property `:read-only` to run MathB in read-only mode.
115 | - Runtime property `:max-title-length` to specify maximum title
116 | length.
117 | - Runtime property `:max-name-length` to specify maximum name length.
118 | - Runtime property `:max-code-length` to specify maximum code length.
119 | - Runtime property `:global-post-interval` to specify minimum interval
120 | between two posts from arbitrary clients.
121 | - Runtime property `:client-post-interval` to specify minimum interval
122 | between two posts from the same client.
123 | - Runtime property `:block` to specify blocked strings in post.
124 |
125 |
126 | ### Changed
127 |
128 | - No more PHP source code. All application code is in Common Lisp
129 | now.
130 |
131 |
132 | 0.2.0 (2020-11-28)
133 | ------------------
134 |
135 | ### Added
136 |
137 | - Stop rendering `` elements.
138 |
139 |
140 | 0.1.0 (2013-12-02)
141 | ------------------
142 |
143 | ### Added
144 |
145 | - Support Markdown format.
146 | - Reset equation numbers while rendering input.
147 |
148 |
149 | 0.0.1 (2012-03-25)
150 | ------------------
151 |
152 | ### Added
153 |
154 | - The first release of MathB written in PHP.
155 |
--------------------------------------------------------------------------------
/web/js/mathb/mathb.js:
--------------------------------------------------------------------------------
1 | window.onload = function () {
2 | const form = document.getElementById('form')
3 | const code = document.getElementById('code')
4 | const title = document.getElementById('title')
5 | const name = document.getElementById('name')
6 | const submit = document.getElementById('submit')
7 | const view = document.getElementById('view')
8 | const error = document.getElementById('error')
9 | const widget = document.getElementById('widget')
10 | const url = document.getElementById('url')
11 | const copy = document.getElementById('copy')
12 | const slug = parseInt(window.location.pathname.substring(1))
13 |
14 | let rendering = false
15 | let timeout = null
16 |
17 | const allowedNodes = [
18 | "#text", "blockquote", "br", "code", "div", "em",
19 | "h1", "h2", "h3", "h4", "h5", "h6",
20 | "hr", "li", "ol", "p", "pre", "strong",
21 | "table", "tbody", "td", "tfoot", "th", "thead", "tr", "ul"
22 | ]
23 |
24 | function removeMarkup(s) {
25 | const div = document.createElement('div')
26 | div.innerHTML = s
27 | return div.innerText
28 | }
29 |
30 | function sanitizeDOM(node) {
31 | if (allowedNodes.indexOf(node.nodeName.toLowerCase()) === -1) {
32 | node.parentNode.removeChild(node)
33 | return
34 | }
35 | if (node.nodeType === Node.ELEMENT_NODE) {
36 | for (let i = 0; i < node.attributes.length; i++) {
37 | node.removeAttributeNode(node.attributes[i])
38 | }
39 | }
40 | for (let i = node.childNodes.length - 1; i >= 0; i--) {
41 | sanitizeDOM(node.childNodes[i])
42 | }
43 | }
44 |
45 | function sanitizeHTML(html) {
46 | if (slug < 10) {
47 | return html
48 | }
49 | const div = document.createElement('div')
50 | div.innerHTML = html
51 | sanitizeDOM(div)
52 | return div.innerHTML
53 | }
54 |
55 | function renderView() {
56 | if (rendering) {
57 | return
58 | }
59 | rendering = true
60 |
61 | const titleValue = title.value.trim()
62 | const nameValue = name.value.trim()
63 | const codeValue = code.value.trim()
64 |
65 | let h1 = ''
66 | let h2 = ''
67 | let body = ''
68 |
69 | if (titleValue !== '') {
70 | h1 = '
'
75 | }
76 |
77 | if (codeValue != '') {
78 | body = sanitizeHTML(texme.render(codeValue))
79 | }
80 |
81 | view.innerHTML = h1 + h2 + body
82 | window.MathJax.texReset()
83 | window.MathJax.typesetPromise().then(function () {
84 | rendering = false
85 | })
86 | }
87 |
88 | // Schedule input handler to process input after a short delay.
89 | //
90 | // When the user edits an element in the input form, the
91 | // corresponding element of the output sheet is not updated
92 | // immediately for two reasons:
93 | //
94 | // - A fast typist can type 7 to 10 characters per second.
95 | // Updating the output sheet so frequently, causes the user
96 | // interface to become less responsive.
97 | //
98 | // - The onpaste or oncut functions of an input element gets the
99 | // old value of the element instead of the new value resulting
100 | // from the cut or paste operation.
101 | //
102 | // This function works around the above issues by scheduling the
103 | // renderView() function to be called after 100 milliseconds. This
104 | // ensures that the output is not updated more than 10 times per
105 | // second. This also ensures that when the renderView() function is
106 | // invoked as a result of a cut or paste operation on a text field
107 | // element, then it gets the updated value of the element.
108 | function scheduleInputHandler() {
109 | if (timeout !== null) {
110 | window.clearTimeout(timeout)
111 | timeout = null
112 | }
113 | timeout = window.setTimeout(renderView, 100)
114 | }
115 |
116 | function insertToken() {
117 | const a = 123 + Math.floor((1000 - 123) * Math.random())
118 | const b = a % 91
119 | const c = a % 87
120 | const x = 1000000 * a + 1000 * b + c
121 | const input = document.createElement('input')
122 | input.setAttribute('type', 'hidden')
123 | input.setAttribute('name', 'token')
124 | input.setAttribute('value', x.toString())
125 | form.appendChild(input)
126 | }
127 |
128 | function copyURL() {
129 | url.select()
130 | document.execCommand('copy')
131 | window.setTimeout(function () { url.blur() }, 125)
132 | }
133 |
134 | function init() {
135 | form.onsubmit = insertToken
136 |
137 | code.onkeyup = scheduleInputHandler
138 | code.onpaste = scheduleInputHandler
139 | code.oncut = scheduleInputHandler
140 |
141 | title.onkeyup = scheduleInputHandler
142 | title.onpaste = scheduleInputHandler
143 | title.oncut = scheduleInputHandler
144 |
145 | name.onkeyup = scheduleInputHandler
146 | name.onpaste = scheduleInputHandler
147 | name.oncut = scheduleInputHandler
148 |
149 | url.onclick = url.select
150 | copy.onclick = copyURL
151 |
152 | code.focus()
153 |
154 | if (window.location.pathname !== '/') {
155 | url.value = window.location.href
156 | }
157 |
158 | if (error === null) {
159 | renderView()
160 | }
161 | }
162 |
163 | init()
164 | }
165 |
--------------------------------------------------------------------------------
/meta/data/post/0/0/2.txt:
--------------------------------------------------------------------------------
1 | Date: 2012-03-25 00:00:00 +0000
2 | Title: Get Started
3 | Name:
4 |
5 | Inline Mathematics
6 | ------------------
7 |
8 | Inline mathematics can be written in two ways.
9 | They can be enclosed within \begin{md}`$`\end{md}
10 | and \begin{md}`$`\end{md}, or
11 | \begin{md}`\(`\end{md} and \begin{md}`\)`\end{md}.
12 | For example, typing `$ e^{i \pi} + 1 = 0 $` or `\(
13 | e^{i \pi} + 1 = 0 \)` in the code pane produces
14 | the following output in the output pane: $ e^{i
15 | \pi} + 1 = 0 $.
16 |
17 |
18 | Displayed Mathematics
19 | ---------------------
20 |
21 | Displayed mathematics appears on its own line in
22 | the output. To render displayed math, they can be
23 | enclosed within \begin{md}`$$`\end{md} and
24 | \begin{md}`$$`\end{md}, or \begin{md}`\[`\end{md}
25 | and \begin{md}`\]`\end{md}. For example, typing
26 | `$$ e^{i \pi} + 1 = 0. $$` or `\[ e^{i \pi} + 1 =
27 | 0. \]` in the code pane produces the following
28 | output in the output pane: $$ e^{i \pi} + 1 = 0.
29 | $$ Apart from this, many LaTeX environments such
30 | as `equation`, `align`, etc. are also supported.
31 |
32 |
33 | Text
34 | ----
35 |
36 | Mathematics and text can be mixed freely while
37 | posting a mathematics snippet. Text is written in
38 | Markdown format. As you can see, this tutorial
39 | itself is another post, so you can compare the
40 | code in the code pane with the output in output
41 | pane as you read this tutorial. A new paragraph
42 | begins by placing a blank line in the code.
43 |
44 | This is a new paragraph. Check the code pane on
45 | to ensure that there indeed is a blank line
46 | between this paragraph and the previous one.
47 |
48 | A code fence can be created by writing three
49 | consecutive backticks (` ``` `) on its own line.
50 | Code blocks are placed between two code fences:
51 |
52 | ```
53 | #include
54 |
55 | int main(int argc, char **argv)
56 | {
57 | printf("hello, world\n");
58 | }
59 | ```
60 |
61 | Ordered lists can be written by numbering each
62 | line:
63 |
64 | 1. Apple
65 | 2. Mango
66 | 3. Banana
67 |
68 | Unordered lists can be written by using an
69 | asterisk (`*`), plus sign (`+`), or minus sign
70 | (`-`) as bullet for each item in the list:
71 |
72 | - Apple
73 | - Mango
74 | - Banana
75 |
76 | Enclose text within a pair of asterisks to
77 | *emphasize* it. Enclose text within a pair of
78 | double-asterisks to **emphasize it strongly**.
79 | Underscores may be used instead of asterisks to
80 | add emphasis too.
81 |
82 | Various other features are supported in Markdown
83 | such as inline code, blockquotes, horizontal
84 | rules, etc. This website conforms to GitHub
85 | Flavoured Markdown (GFM) specification. Note that
86 | GFM is a strict superset of CommonMark.
87 |
88 |
89 | LaTeX
90 | -----
91 |
92 | This website uses [MathJax][M1] to render
93 | mathematics on the web browser. See [MathJax:
94 | Supported TeX/LaTeX commands][M2] for a list of
95 | LaTeX commands supported by MathJax.
96 |
97 |
98 | Markdown
99 | --------
100 |
101 | This website supports GitHub Flavoured Markdown
102 | (GFM). GFM is a strict superset of the CommonMark
103 | specification of Markdown. See the [GitHub
104 | Flavoured Markdown Specification][G1] for more
105 | details on the specification.
106 |
107 |
108 | TeXMe
109 | -----
110 |
111 | This website uses [TeXMe][T1] to allow mixing
112 | LaTeX with Markdown. TeXMe reads the input and
113 | renders it while preventing the Markdown renderer
114 | from seeing the LaTeX code, so that the LaTeX code
115 | remains intact for rendering by MathJax.
116 |
117 | However, when Markdown code contains mathematics
118 | delimiters like \begin{md}`$`\end{md},
119 | \begin{md}`\(`\end{md}, etc., TeXMe may identify
120 | them as mathematics instead of Markdown code as
121 | intended. To prevent mathematics delimiters in
122 | Markdown code from being interpreted as LaTeX,
123 | enclose the Markdown code within
124 | \begin{mdx}`\begin{md}`\end{mdx} and
125 | \begin{mdx}`\end{md}`\end{mdx}. Here is an
126 | example:
127 |
128 | \begin{mdx}
129 | ```
130 | > The variables \begin{md}`$foo`\end{md} and
131 | > \begin{md}`$bar`\end{md} are metasyntactic
132 | > variables.
133 | ```
134 | \end{mdx}
135 |
136 | Here is how the above code is rendered in the
137 | output:
138 |
139 | > The variables \begin{md}`$foo`\end{md} and
140 | > \begin{md}`$bar`\end{md} are metasyntactic
141 | > variables.
142 |
143 | See [TeXMe: Markdown Priority Environment][T2] to
144 | learn more about this.
145 |
146 |
147 | Print
148 | -----
149 |
150 | While the primary purpose of this website is to
151 | allow users to write mathematical snippets, save
152 | them, and share a link to them with others, the
153 | stylesheet used in this website takes special care
154 | to allow printing beautifully rendered pages to
155 | paper or PDF.
156 |
157 | When a page from this website is printed, only the
158 | rendered content appears in the print. The input
159 | form, buttons, navigation links, and other user
160 | interface elements do not appear in the print.
161 |
162 |
163 | Save PDF
164 | --------
165 |
166 | It is possible to turn a post on this website into
167 | a PDF file using the printing facility of most web
168 | browsers running on a desktop or laptop. The
169 | exact steps to save a web page as PDF vary from
170 | browser to browser but the steps to do so roughly
171 | look like this:
172 |
173 | - Select File > Print from the web browser menu.
174 | - Then in the print window or dialog box that
175 | comes up, deselect/disable the options to print
176 | headers and footers.
177 | - Finally, choose the option to save a PDF.
178 |
179 | You can try the above steps right now with this
180 | page and save this page as PDF.
181 |
182 | If everything works as expected, the saved PDF
183 | should contain only the rendered content with all
184 | mathematical formulas rendered properly. This
185 | website has special styling rules to ensure that
186 | the input form, buttons, navigation links, and
187 | other user interface elements do not appear in
188 | saved PDF.
189 |
190 |
191 | [M1]: http://www.mathjax.org/
192 | [M2]: https://docs.mathjax.org/en/latest/input/tex/macros/
193 | [G1]: https://github.github.com/gfm/
194 | [T1]: https://github.com/susam/texme#texme
195 | [T2]: https://github.com/susam/texme#markdown-priority-environment
196 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | NAME = mathb
2 | FQDN = $(NAME).in
3 | MAIL = $(FQDN)@yahoo.com
4 |
5 | help:
6 | @echo 'Usage: make [target]'
7 | @echo
8 | @echo 'Targets to run on live server:'
9 | @echo ' setup Install Debian packages and Quicklisp for website.'
10 | @echo ' https Reinstall live website and serve with Nginx via HTTPS.'
11 | @echo ' http Reinstall live website and serve with Nginx via HTTP.'
12 | @echo ' rm Uninstall live website.'
13 | @echo ' backup Create live server data backup.'
14 | @echo ' review Review posts listed in /tmp/review.txt.'
15 | @echo ' follow-log Follow logs on live server.'
16 | @echo ' follow-post Follow POST logs on live server.'
17 | @echo ' post-log Filter logs to find all successful posts.'
18 | @echo ' top-get-log Filter logs to find most popular pages.'
19 | @echo
20 | @echo 'Low-level targets:'
21 | @echo ' live Generate live website.'
22 | @echo ' site Generate local website.'
23 | @echo
24 | @echo 'Development targets:'
25 | @echo ' opt Create directories at /opt for testing.'
26 | @echo ' run Run application.'
27 | @echo ' test Test application code.'
28 | @echo ' checks Validate consistency within configuration.'
29 | @echo ' pub Publish updated website on live server.'
30 | @echo ' force-pub Reset website on live server and publish website.'
31 | @echo ' pull-backup Pull a backup of data from live server.'
32 | @echo
33 | @echo 'Default target:'
34 | @echo ' help Show this help message.'
35 |
36 |
37 | # Targets for Live Server
38 | # -----------------------
39 |
40 | setup:
41 | apt-get update
42 | apt-get -y install nginx certbot sbcl
43 | rm -rf /opt/quicklisp.lisp /opt/quicklisp
44 | curl https://beta.quicklisp.org/quicklisp.lisp -o /opt/quicklisp.lisp
45 | sbcl --load /opt/quicklisp.lisp \
46 | --eval '(quicklisp-quickstart:install :path "/opt/quicklisp/")' \
47 | --quit
48 | chown -R www-data:www-data /opt/quicklisp
49 |
50 | https: http wait-http
51 | @echo Setting up HTTPS website ...
52 | certbot certonly -n --agree-tos -m '$(MAIL)' --webroot \
53 | -w '/var/www/$(FQDN)' -d '$(FQDN),www.$(FQDN)'
54 | (crontab -l | sed '/::::/d'; cat etc/crontab) | crontab
55 | ln -snf "$$PWD/etc/nginx/https.$(FQDN)" '/etc/nginx/sites-enabled/$(FQDN)'
56 | systemctl restart nginx
57 | @echo Done; echo
58 |
59 | http: rm live mathb
60 | @echo Setting up HTTP website ...
61 | ln -snf "$$PWD/_live" '/var/www/$(FQDN)'
62 | ln -snf "$$PWD/etc/nginx/http.$(FQDN)" '/etc/nginx/sites-enabled/$(FQDN)'
63 | ln -snf "$$PWD/etc/logrotate" /etc/logrotate.d/mathb
64 | systemctl restart nginx
65 | echo 127.0.0.1 '$(NAME)' >> /etc/hosts
66 | @echo Done; echo
67 |
68 | wait-http:
69 | @echo Waiting for HTTP website to start ...
70 | while ! curl http://localhost:4242/; do sleep 1; echo Retrying ...; done
71 | @echo Done; echo
72 |
73 | mathb:
74 | @echo Setting up mathb ...
75 | mkdir -p /opt/cache/ /opt/data/mathb/ /opt/log/mathb/
76 | chown -R www-data:www-data /opt/cache/ /opt/data/mathb/ /opt/log/mathb/
77 | systemctl enable "/opt/mathb.in/etc/mathb.service"
78 | systemctl daemon-reload
79 | systemctl start mathb
80 | @echo Done; echo
81 |
82 | rm: checkroot
83 | @echo Removing website ...
84 | rm -f /etc/logrotate/mathb
85 | rm -f '/etc/nginx/sites-enabled/$(FQDN)'
86 | rm -f '/var/www/$(FQDN)'
87 | systemctl restart nginx
88 | sed -i '/$(NAME)/d' /etc/hosts
89 | @echo
90 | @echo Removing mathb ...
91 | -systemctl stop mathb
92 | -systemctl disable mathb
93 | systemctl daemon-reload
94 | @echo
95 | @echo Following crontab entries left intact:
96 | crontab -l | grep -v "^#" || :
97 | @echo Done; echo
98 |
99 | checkroot:
100 | @echo Checking if current user is root ...
101 | [ $$(id -u) = 0 ]
102 | @echo Done; echo
103 |
104 | backup:
105 | tar -caf "/opt/cache/mathb-$$(date "+%Y-%m-%d_%H-%M-%S").tgz" -C /opt/data/ mathb/
106 | ls -1 /opt/cache/mathb-*.tgz | sort -r | tail -n +100 | xargs rm -vf
107 | ls -lh /opt/cache/
108 | df -h /
109 |
110 | review: checkroot
111 | mkdir -p /tmp/deleted
112 | [ -f /tmp/review.txt ]
113 | for f in $$(cat /tmp/review.txt); do \
114 | reply=e; \
115 | echo "Editing $$f ..."; \
116 | while [ "$$reply" = e ]; do \
117 | emacs "$$f"; \
118 | echo; head "$$f"; printf "\n\n...\n\n"; tail "$$f"; echo; \
119 | reply=; \
120 | while ! printf '%s' "$$reply" | grep -qE '^(e|d|n|q)$$'; do \
121 | printf "Action for $$f (d, e, n, q)? "; \
122 | read reply; \
123 | [ "$$reply" = d ] && mv "$$f" /tmp/deleted; \
124 | [ "$$reply" = q ] && exit; \
125 | done; \
126 | done; \
127 | done; echo Done; echo
128 |
129 | follow-log:
130 | tail -F /opt/log/mathb/mathb.log
131 |
132 | follow-post:
133 | tail -F /opt/log/mathb/mathb.log | grep POST
134 |
135 | post-log:
136 | tail -F /opt/log/mathb/mathb.log | grep written
137 |
138 | top-get-log:
139 | grep ' 200 ' /opt/log/mathb/mathb.log* | grep -o 'GET /[0-9]*' | sort | uniq -c | sort -nr | nl | less
140 |
141 |
142 | # Low-Level Targets
143 | # -----------------
144 |
145 | live: site
146 | @echo Setting up live directory ...
147 | mv _live _gone || :
148 | mv _site _live
149 | rm -rf _gone
150 | @echo Done; echo
151 |
152 | site:
153 | @echo Setting up site directory ...
154 | rm -rf _site/
155 | mkdir -p _site/css/ _site/js/
156 | cp -R web/css/* _site/css/
157 | cp -R web/js/* _site/js/
158 | cp -R web/img/* _site/
159 | git -C _site/js/ clone -b 1.2.0 --depth 1 https://github.com/susam/texme.git
160 | git -C _site/js/ clone -b v4.1.0 --depth 1 https://github.com/markedjs/marked.git
161 | git -C _site/js/ clone -b 3.2.2 --depth 1 https://github.com/mathjax/mathjax.git
162 | rm -rf _site/js/texme/.git
163 | rm -rf _site/js/marked/.git/
164 | rm -rf _site/js/mathjax/.git/
165 | @echo Done; echo
166 |
167 |
168 | # Development Targets
169 | # -------------------
170 |
171 | opt:
172 | sudo mkdir -p /opt/data/mathb/ /opt/log/mathb/
173 | sudo cp -R meta/data/* /opt/data/mathb/
174 | sudo chown -R "$$USER" /opt/data/mathb/ /opt/log/mathb/
175 |
176 | run:
177 | sbcl --load mathb.lisp
178 |
179 | test:
180 | sbcl --noinform --eval "(defvar *quit* t)" --load test.lisp
181 |
182 | checks:
183 | # Ensure http.mathb.in and https.mathb.in are consistent.
184 | sed -n '1,/limit_req_status/p' etc/nginx/http.mathb.in > /tmp/http.mathb.in
185 | sed -n '1,/limit_req_status/p' etc/nginx/https.mathb.in > /tmp/https.mathb.in
186 | diff -u /tmp/http.mathb.in /tmp/https.mathb.in
187 | sed -n '/server_name [^w]/,/^}/p' etc/nginx/http.mathb.in > /tmp/http.mathb.in
188 | sed -n '/server_name [^w]/,/^}/p' etc/nginx/https.mathb.in > /tmp/https.mathb.in
189 | diff -u /tmp/http.mathb.in /tmp/https.mathb.in
190 | @echo Done; echo
191 |
192 | pub:
193 | git push
194 | ssh -t mathb.in "cd /opt/mathb.in/ && sudo git pull && sudo cp meta/data/post/0/0/*.txt /opt/data/mathb/post/0/0/ && sudo chown -R www-data:www-data meta/data/post/0/0/*.txt && sudo make live && sudo systemctl restart nginx mathb && sudo systemctl --no-pager status nginx mathb"
195 |
196 | force-pub:
197 | git push -f
198 | ssh -t mathb.in "cd /opt/mathb.in/ && sudo git reset --hard HEAD~5 && sudo git pull && sudo cp meta/data/post/0/0/*.txt /opt/data/mathb/post/0/0/ && sudo chown -R www-data:www-data meta/data/post/0/0/*.txt && sudo make live && sudo systemctl restart nginx mathb && sudo systemctl --no-pager status nginx mathb"
199 |
200 | pull-backup:
201 | mkdir -p ~/bkp/
202 | ssh mathb.in "tar -czf - -C /opt/data/ mathb/" > ~/bkp/mathb-$$(date "+%Y-%m-%d_%H-%M-%S").tgz
203 | ls -lh ~/bkp/
204 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | MathB
2 | =====
3 |
4 | MathB is a mathematics pastebin software that powered `MathB.in` from
5 | 2012 to 2025. It is a web-based service meant for sharing snippets of
6 | mathematical text with others on the world wide web.
7 |
8 |
9 | Contents
10 | --------
11 |
12 | * [Features](#features)
13 | * [Quick Start](#quick-start)
14 | * [Custom Directory Paths](#custom-directory-paths)
15 | * [Data Files](#data-files)
16 | * [Runtime Options](#runtime-options)
17 | * [Templates Files](#template-files)
18 | * [Static Files](#static-files)
19 | * [Live Directory](#live-directory)
20 | * [Print](#print)
21 | * [Save PDF](#save-pdf)
22 | * [History](#history)
23 | * [License](#license)
24 | * [Support](#support)
25 | * [Channels](#channels)
26 | * [More](#more)
27 |
28 |
29 | Features
30 | --------
31 |
32 | - Minimalist user interface that has not changed much over a decade.
33 | - Live preview of Markdown and LaTeX content as it is typed.
34 | - Support for mixing Markdown and LaTeX code freely.
35 | - Printing a post to PDF or paper prints only the rendered content.
36 | - All UI elements apart from rendered content are excluded from
37 | prints.
38 | - No web cookies.
39 | - No web analytics.
40 |
41 |
42 | Quick Start
43 | -----------
44 |
45 | This section explains how to run this project locally. The steps
46 | assume a macOS, Debian, or Debian-based Linux distribution. However,
47 | it should be possible to adapt these steps for another operating
48 | system.
49 |
50 | 1. Install SBCL and Git.
51 |
52 | On macOS, enter the following command if you have Homebrew:
53 |
54 | ```sh
55 | brew install sbcl git
56 | ```
57 |
58 | On Debian, Ubuntu, or another Debian-based Linux system, enter the
59 | following command:
60 |
61 | ```sh
62 | sudo apt-get update
63 | sudo apt-get install sbcl git
64 | ```
65 |
66 | 2. Install Quicklisp with the following commands:
67 |
68 | ```sh
69 | curl -O https://beta.quicklisp.org/quicklisp.lisp
70 | sbcl --load quicklisp.lisp --eval "(quicklisp-quickstart:install)" --quit
71 | sbcl --load ~/quicklisp/setup.lisp --eval "(ql:add-to-init-file)" --quit
72 | ```
73 |
74 | 3. From here on, we assume that all commands are being run in the
75 | top-level directory of this project. Set up dependencies
76 | necessary to run this project by running this command within the
77 | top-level directory of this project:
78 |
79 | ```sh
80 | make live
81 | ```
82 |
83 | This creates a `_live` directory within the current directory and
84 | copies all necessary dependencies to it.
85 |
86 | 4. Create data and log directories:
87 |
88 | ```sh
89 | sudo mkdir -p /opt/data/mathb/ /opt/log/mathb/
90 | sudo cp -R meta/data/* /opt/data/mathb/
91 | sudo chown -R "$USER" /opt/data/mathb/ /opt/log/mathb/
92 | ```
93 |
94 | By default, MathB reads post data from and writes posts to
95 | `/opt/data/mathb/`. It writes logs to `/opt/log/mathb/` by
96 | default. The next section explains how to make it use custom
97 | directory paths.
98 |
99 | 4. Run MathB with the following command:
100 |
101 | ```sh
102 | sbcl --load mathb.lisp
103 | ```
104 |
105 | 5. Visit http://localhost:4242/ with a web browser to use MathB.
106 |
107 | After starting MathB in this manner, click on the various navigation
108 | links and make a new post to confirm that MathB is working as
109 | expected.
110 |
111 |
112 | Custom Directory Paths
113 | ----------------------
114 |
115 | In the previous section, we created a data directory at
116 | `/opt/data/mathb/` and a log directory at `/opt/log/mathb/`. By
117 | default, MathB writes new posts to and reads posts from this directory
118 | path. To make it use a different path for the data directory, set the
119 | variable named `*data-directory*` before loading it. Similarly, set
120 | the variable named `*log-directory*` to specify a different path for
121 | the log directory. The following steps demonstrate how to do this:
122 |
123 | 1. Create data directory at a custom path, say, at `~/data`:
124 |
125 | ```sh
126 | mkdir -p ~/data/ ~/log/
127 | cp -R meta/data/* ~/data/
128 | ```
129 |
130 | 2. Run MathB with the following command:
131 |
132 | ```sh
133 | sbcl --eval '(defvar *data-directory* "~/data/")' \
134 | --eval '(defvar *log-directory* "~/log/")' \
135 | --load mathb.lisp
136 | ```
137 |
138 | 3. Visit http://localhost:4242/ with a web browser to use MathB.
139 |
140 | After starting MathB in this manner, click on the various navigation
141 | links and make a new post to confirm that MathB is working as
142 | expected.
143 |
144 |
145 | Data Files
146 | ----------
147 |
148 | The data directory contains the following files:
149 |
150 | - [`opt.lisp`]: This file contains a property list that can be
151 | modified to alter the behaviour of MathB. This is explained in
152 | detail in the next section.
153 |
154 | - [`slug.txt`]: This file contains the ID of the latest post
155 | successfully saved.
156 |
157 | - [`post/X/Y/*.txt`]: These files contain the actual posts submitted
158 | by users where `X` and `Y` are placeholders for two integers
159 | explained shortly. Each `.txt` file contains a post submitted by a
160 | user.
161 |
162 | In the last point, the placeholder `X` is the post ID divided
163 | by 1000000. The placeholder `Y` is the post ID divided by 1000. For
164 | example, for a post with ID 1, `X` is `0` and `Y` is `0`, so a post
165 | with this ID is saved at `post/0/0/1.txt`. For a more illustrative
166 | example, consider a post with with ID 2301477. Now `X` is `2` and `Y`
167 | is `2301`, so a post with this ID is saved at
168 | `post/2/2301/2301477.txt`.
169 |
170 | Let us call each `X` directory a short-prefix directory and each `Y`
171 | directory under it a long-prefix directory. As a result of the
172 | calculation explained above, each short-prefix directory contains a
173 | maximum of 1000 long-prefix directories and each long-prefix directory
174 | contains a maximum of 1000 post files. Thus, each short-prefix
175 | directory contains a maximum of one million post files under it.
176 |
177 | [`opt.lisp`]: meta/data/opt.lisp
178 | [`slug.txt`]: meta/data/slug.txt
179 | [`post/X/Y/*.txt`]: meta/data/post/0/0
180 |
181 |
182 | Runtime Options
183 | ---------------
184 |
185 | MathB reads runtime properties from `opt.lisp`. This file contains a
186 | property list. Each property in this list is followed by a value for
187 | that property. This property list may be used to alter the behaviour
188 | of MathB. A list of all supported properties and their descriptions
189 | is provided below.
190 |
191 | - `:lock-down` (default is `nil`): A value of `t` makes MathB run in
192 | lock-down mode, i.e., existing posts cannot be viewed and new
193 | posts cannot be submitted.
194 |
195 | - `:read-only` (default is `nil`): A value of `t` makes MathB run in
196 | read-only mode, i.e., old posts can be viewed but new posts cannot
197 | be made. If the values of both this property and the previous
198 | property are `nil`, then MathB runs normally in read-write mode.
199 |
200 | - `:min-title-length` (default is `0`): The minimum number of
201 | characters allowed in the title field.
202 |
203 | - `:max-title-length` (default is `120`): The maximum number of
204 | characters allowed in the title field.
205 |
206 | - `:min-name-length` (default is `0`): The minimum number of
207 | characters allowed in the name field.
208 |
209 | - `:max-name-length` (default is `120`): The maximum number of
210 | characters allowed in the name field.
211 |
212 | - `:min-code-length` (default is `1`): The minimum number of
213 | characters allowed in the code field.
214 |
215 | - `:max-code-length` (default is `10000`): The maximum number of
216 | characters allowed in the code field.
217 |
218 | - `:global-post-interval` (default is `0`): The minimum interval (in
219 | seconds) required between two consecutive successful posts.
220 |
221 | Example: If this value is `10` and one client submits a new post
222 | at 10:00:00 and another client submits a post at 10:00:07, the
223 | post of the second client is rejected with an error message that
224 | they must wait for 3 more seconds before submitting the post. An
225 | attempt to submit the post at 10:00:10 or later would succeed,
226 | provided that no other client submitted another post between
227 | 10:00:10 and the second client's attempt to make a post.
228 |
229 | - `:client-post-interval` (default is `0`): The minimum interval (in
230 | seconds) between two consecutive successful posts allowed from the
231 | same client.
232 |
233 | Example: If this value is `10` and one client submits a new post
234 | at 10:00:00, then the same client is allowed to make the next
235 | successful post submission at 10:00:10 or later. If the same
236 | client submits another post at 10:00:07, the post is rejected with
237 | an error message that they must wait for 3 more seconds before
238 | submitting the post. This does not affect the posting behaviour
239 | for other clients. For example, another client can successfully
240 | submit their post at 10:00:07 while the first client cannot.
241 |
242 | - `:expect` (default is `nil`): A list of strings. At least one
243 | string from this list must occur in the submitted code field.
244 |
245 | Example: If this value is `("\(" "\[")` and the submitted post
246 | contains `\[ 1 + 1 = 2. \]` in the code field, then the post is
247 | accepted successfully. However, if the submitted code contains
248 | only `1 + 1 = 2`, then the post is rejected because neither the
249 | string `"\("` nor the string `"\["` occurs in the code field of
250 | this submission.
251 |
252 | - `:block` (default is `nil`): A list of strings that are not
253 | allowed in a post. If a post contains any string in this list,
254 | the post is rejected and the input form is returned intact to the
255 | client.
256 |
257 | Example: If this value is `("berk" "naff" "xxx")` and a client
258 | posts content which contains the string `xxx` in any field (code,
259 | title, or name), the post is rejected.
260 |
261 | - `:ban` (default is `nil`): A list of IPv4 or IPv6 address
262 | prefixes. If the address of the remote client (as it appears in
263 | the logs) matches any prefix in this list, the post from the
264 | client is rejected. The prefixes must be expressed as simple
265 | string literals. CIDRs, globs, regular expressions, etc. are not
266 | supported. A dollar sign (`$`) at the end of a prefix string
267 | matches the end of the client's address string.
268 |
269 | Example: Let us consider a value of `("10.1." "10.2.0.2"
270 | "10.3.0.2$")` for this property. If a client from IP address
271 | `10.1.2.3` submits a post, it is rejected because the prefix
272 | `10.1.` matches this IP address. If a client from IP address
273 | `10.2.0.23` submits a post, it is rejected because the prefix
274 | `10.2.0.2` matches this IP address. If a client from IP address
275 | `10.3.0.2` submits a post, it is rejected because the prefix
276 | `10.3.0.2$` matches this IP address. If a client from IP address
277 | `10.3.0.23` submits a post, it is accepted because none of the
278 | prefixes match this IP address.
279 |
280 | - `:protect` (default is `0`): The maximum ID of protected posts.
281 | If MathB determines that the post ID of the next post is less than
282 | or equal to this value, then it rejects the post. Setting this
283 | property is almost never required. However, it is provided for
284 | paranoid administrators who might worry what would happen if the
285 | data file `slug.txt` ever becomes corrupt. This property ensures
286 | that in case this data file ever becomes corrupt, MathB would
287 | never ever overwrite old posts with IDs less than or equal to the
288 | number set for this property.
289 |
290 | Example: Let us assume that the current value in `slug.txt`
291 | is 1200. Now normally, the next time a client submits a new post,
292 | their post would be saved with an ID of 1201 and the value in
293 | `slug.txt` would be incremented to 1201. But instead, let us
294 | assume that due to an unforeseen scenario (say, a bug in MathB or
295 | a hardware failure), the value in `slug.txt` is corrupted to `12`.
296 | With a value of `0` for `:protect`, MathB would overwrite an
297 | existing post at `post/0/0/13.txt`. However, with a value of say,
298 | `100` for `:protect`, MathB would refuse to overwrite the existing
299 | port.
300 |
301 | If a property name is missing from this file or if the file itself is
302 | missing, then the default value of the property mentioned within
303 | parentheses above is used.
304 |
305 | Whenever a post is rejected due to a runtime option, the entire input
306 | form is returned intact to the client with an error message, so that
307 | they can fix the errors or wait for the suggested post interval and
308 | resubmit the post again.
309 |
310 | The property values in `opt.lisp` may be modified at any time, even
311 | while MathB is running. It is not necessary to restart MathB after
312 | changing property values in `opt.lisp`. The changes are picked up
313 | automatically while processing the next HTTP POST request.
314 |
315 |
316 | Template Files
317 | --------------
318 |
319 | There are two template files to generate the HTML pages sent to the
320 | clients:
321 |
322 | - [`web/html/mathb.html`]: This template file is used to generate
323 | the HTML response for the home page, a mathematical snippet page,
324 | as well as an HTTP response page when the post is rejected due to
325 | a validation error.
326 |
327 | - [`web/html/error.html`]: This template file is used to generate
328 | HTTP error pages.
329 |
330 | A template file may be modified at any time, even while MathB is
331 | running. It is not necessary to restart MathB after changing a
332 | template file. The changes are picked up automatically while
333 | processing the next HTTP request.
334 |
335 | [`web/html/mathb.html`]: web/html/mathb.html
336 | [`web/html/error.html`]: web/html/error.html
337 |
338 |
339 | Static Files
340 | ------------
341 |
342 | There are three types of static files that MathB uses to for its HTML
343 | pages:
344 |
345 | - [`web/js/`]: This directory contains the JavaScript files that
346 | perform input rendering as a user types out content in the input
347 | form.
348 |
349 | - [`web/css/`]: This directory contains the stylesheets for the HTML
350 | pages generated by MathB.
351 |
352 | - [`web/img/`]: This directory contains the favicons for the
353 | website. These icons are generated using a LaTeX project in the
354 | [`meta/logo/`] directory.
355 |
356 | A static file may be modified at any time, even while MathB is
357 | running. It is not necessary to restart MathB after adding, deleting,
358 | or editing a static file. However, it is necessary to run `make live`
359 | (in the top-level directory of the project) to copy the static files
360 | to the live directory (explained in the next section) from which MathB
361 | serves the static files.
362 |
363 | [`web/js/`]: web/js/
364 | [`web/css/`]: web/css/
365 | [`web/img/`]: web/img/
366 | [`meta/logo/`]: meta/logo/
367 |
368 |
369 | Live Directory
370 | --------------
371 |
372 | MathB needs to pull additional JavaScript libraries named TeXMe,
373 | Marked, and MathJax that are essential for rendering Markdown and
374 | LaTeX input. This is done by running the following command in the
375 | top-level directory of this project:
376 |
377 | ```sh
378 | make live
379 | ```
380 |
381 | The above command creates a `_live` directory from scratch, copies the
382 | static files to it, then pulls the additional JavaScript libraries
383 | into it, and sets up the `_live` directory, so that MathB can serve
384 | the static files from it.
385 |
386 | The live directory should never be modified directly because every
387 | `make live` run deletes the entire directory and creates it from
388 | scratch again. Any modification necessary should be made to the
389 | template files or static files explained in the previous two sections.
390 |
391 |
392 | Print
393 | -----
394 |
395 | While the primary purpose of this project is to allow users to write
396 | mathematical snippets, save them, and share a link to them with
397 | others, the stylesheet used in this project takes special care to
398 | allow printing beautifully rendered pages to paper.
399 |
400 | When a MathB page is printed, only the rendered content appears in the
401 | print. The input form, buttons, navigation links, and other user
402 | interface elements do not appear in the print.
403 |
404 |
405 | Save PDF
406 | --------
407 |
408 | It is possible to turn a MathB post into a PDF file using the printing
409 | facility of most web browsers running on a desktop or laptop. The
410 | exact steps to save a web page as PDF vary from browser to browser but
411 | the steps to do so look roughly like this:
412 |
413 | - Select File > Print from the web browser menu.
414 | - Then in the print window or dialog box that comes up,
415 | deselect/disable the options to print headers and footers.
416 | - Finally, choose the option to save a PDF.
417 |
418 | If everything works as expected, the saved PDF should contain only the
419 | rendered content with all mathematical formulas rendered properly.
420 | The web pages generated by this project use special styling rules to
421 | ensure that the input form, buttons, navigation links, and other user
422 | interface elements do not appear in saved PDF.
423 |
424 |
425 | History
426 | -------
427 |
428 | MathB.in was the longest-running mathematics pastebin, serving its
429 | community of users for over a decade until its discontinuation in
430 | March 2025. It was not the first mathematics pastebin on the world
431 | wide web though. It was the second one. The first one was created by
432 | Mark A. Stratman which was hosted at `mathbin.net` until 2020.
433 |
434 | MathB.in was launched on Sunday, 25 March 2012, after a single night
435 | of furious coding. This was a result of stumbling upon
436 | [math.stackexchange.com][] the previous night, which used MathJax to
437 | render mathematical formulas in a web browser. Thanks to that chance
438 | encounter with MathJax, the rest of that Saturday night was spent
439 | coding a new mathematics pastebin using MathJax and PHP. After coding
440 | through the night, registering a domain name, and setting up a
441 | website, MathB.in was released early Sunday morning.
442 |
443 | Over the years, MathB.in received occasional refinements. It was
444 | eventually rewritten in Common Lisp, replacing its original PHP
445 | implementation. For a detailed account of its evolution, see the post
446 | [MathB.in Turns Ten][].
447 |
448 | MathB.in remained a useful tool for IRC communities, students,
449 | educators, and collaborators for 13 years. It was eventually shut
450 | down on Sunday, 16 March 2025. At the time of its discontinuation, it
451 | held the distinction of being the longest-running mathematics
452 | pastebin. For more details about why this service was discontinued,
453 | see the post [MathB.in Is Shutting Down][].
454 |
455 | Although the original MathB.in service no longer exists, its source
456 | code is still available as free and open-source software through this
457 | project.
458 |
459 | [math.stackexchange.com]: https://math.stackexchange.com/
460 | [MathB.in Turns Ten]: https://susam.net/blog/mathbin-turns-ten.html
461 | [MathB.in Is Shutting Down]: https://susam.net/blog/mathbin-is-shutting-down.html
462 |
463 |
464 | License
465 | -------
466 |
467 | This is free and open source software. You can use, copy, modify,
468 | merge, publish, distribute, sublicense, and/or sell copies of it,
469 | under the terms of the MIT License. See [LICENSE.md][L] for details.
470 |
471 | This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
472 | express or implied. See [LICENSE.md][L] for details.
473 |
474 | [L]: LICENSE.md
475 |
476 |
477 | Support
478 | -------
479 |
480 | To report bugs, suggest improvements, or ask questions, [create
481 | issues][issues].
482 |
483 | [issues]: https://github.com/susam/mathb/issues
484 |
485 |
486 | Channels
487 | --------
488 |
489 | The author of this project hangs out at the following places online:
490 |
491 | - Website: [susam.net](https://susam.net)
492 | - GitHub: [@susam](https://github.com/susam) on GitHub
493 | - Mastodon: [@susam@mastodon.social](https://mastodon.social/@susam)
494 |
495 | You are welcome to subscribe to, follow, or join one or more of the
496 | above channels to receive updates from the author or ask questions
497 | about this project.
498 |
499 |
500 | More
501 | ----
502 |
503 | If you like this project, check out related projects
504 | [TeXMe](https://github.com/susam/texme) and
505 | [Muboard](https://github.com/susam/muboard).
506 |
--------------------------------------------------------------------------------
/mathb.lisp:
--------------------------------------------------------------------------------
1 | ;;;; MathB - A Mathematics Pastebin that Powers MathB.in
2 | ;;;; ===================================================
3 |
4 |
5 | (ql:quickload "hunchentoot")
6 | (require "uiop")
7 |
8 |
9 | ;;; Special Modes
10 | ;;; -------------
11 |
12 | (defvar *log-mode* t
13 | "Write logs iff true.")
14 |
15 | (defvar *main-mode* t
16 | "Run main function iff true.")
17 |
18 |
19 | ;;; General Definitions
20 | ;;; -------------------
21 |
22 | (defun universal-time-string (universal-time-seconds)
23 | "Return given universal time in yyyy-mm-dd HH:MM:SS +0000 format."
24 | (multiple-value-bind (sec min hour date month year)
25 | (decode-universal-time universal-time-seconds 0)
26 | (format nil "~4,'0d-~2,'0d-~2,'0d ~2,'0d:~2,'0d:~2,'0d +0000"
27 | year month date hour min sec)))
28 |
29 | (defun current-utc-time-string ()
30 | "Return current UTC date and time in yyyy-mm-dd HH:MM:SS +0000 format."
31 | (universal-time-string (get-universal-time)))
32 |
33 | (defun directory-exists-p (path)
34 | "Check whether the specified directory exists on the filesystem."
35 | (uiop:directory-exists-p path))
36 |
37 | (defun make-directory (path)
38 | "Create a new directory along with its parents."
39 | (ensure-directories-exist path))
40 |
41 | (defun read-file (filename)
42 | "Read file and close the file."
43 | (uiop:read-file-string filename))
44 |
45 | (defun write-file (filename text)
46 | "Write text to file and close the file."
47 | (make-directory filename)
48 | (with-open-file (f filename :direction :output :if-exists :supersede)
49 | (write-sequence text f)))
50 |
51 | (defun append-file (filename text)
52 | "Append text to file and close the file."
53 | (make-directory filename)
54 | (with-open-file (f filename :direction :output
55 | :if-exists :append
56 | :if-does-not-exist :create)
57 | (write-sequence text f)))
58 |
59 | (defun real-ip ()
60 | "Return address of the remote client (not of the local reverse-proxy)."
61 | (hunchentoot:real-remote-addr))
62 |
63 | (defun string-starts-with (prefix string)
64 | "Test that string starts with the given prefix."
65 | (and (<= (length prefix) (length string))
66 | (string= prefix string :end2 (length prefix))))
67 |
68 | (defun string-replace (old new string)
69 | "Replace non-empty old substring in string with new substring."
70 | (with-output-to-string (s)
71 | (let* ((next-index 0)
72 | (match-index))
73 | (loop
74 | (setf match-index (search old string :start2 next-index))
75 | (unless match-index
76 | (format s "~a" (subseq string next-index))
77 | (return))
78 | (format s "~a~a" (subseq string next-index match-index) new)
79 | (setf next-index (+ match-index (length old)))))))
80 |
81 | (defun string-trim-whitespace (s)
82 | "Trim whitespace from given string."
83 | (string-trim '(#\Space #\Tab #\Return #\Newline) s))
84 |
85 | (defun fix-lines (s)
86 | "Remove carriage returns from string."
87 | (remove #\Return s))
88 |
89 | (defun weekday-name (weekday-index)
90 | "Given an index, return the corresponding day of week."
91 | (nth weekday-index '("Mon" "Tue" "Wed" "Thu" "Fri" "Sat" "Sun")))
92 |
93 | (defun month-name (month-number)
94 | "Given a number, return the corresponding month."
95 | (nth month-number '("X" "Jan" "Feb" "Mar" "Apr" "May" "Jun"
96 | "Jul" "Aug" "Sep" "Oct" "Nov" "Dec")))
97 |
98 | (defun decode-weekday-name (year month date)
99 | "Given a date, return the day of week."
100 | (let* ((encoded-time (encode-universal-time 0 0 0 date month year))
101 | (decoded-week (nth-value 6 (decode-universal-time encoded-time)))
102 | (weekday-name (weekday-name decoded-week)))
103 | weekday-name))
104 |
105 | (defun simple-date (date-string)
106 | "Convert yyyy-mm-dd HH:MM:SS TZ to simple date."
107 | (let* ((year (parse-integer (subseq date-string 0 4)))
108 | (month (parse-integer (subseq date-string 5 7)))
109 | (date (parse-integer (subseq date-string 8 10)))
110 | (hour (parse-integer (subseq date-string 11 13)))
111 | (minute (parse-integer (subseq date-string 14 16)))
112 | (month-name (month-name month))
113 | (weekday-name (decode-weekday-name year month date)))
114 | (format nil "~a, ~2,'0d ~a ~4,'0d ~2,'0d:~2,'0d GMT"
115 | weekday-name date month-name year hour minute)))
116 |
117 | (defun alist-get (key alist)
118 | "Given a key, return its value found in the list of parameters."
119 | (cdr (assoc key alist :test #'string=)))
120 |
121 |
122 | ;;; Tool Definitions
123 | ;;; ----------------
124 |
125 | (defvar *data-directory* "/opt/data/mathb/"
126 | "Directory where post files and data are written to and read from.")
127 |
128 | (defvar *log-directory* "/opt/log/mathb/"
129 | "Directory where log files are written to.")
130 |
131 | (defun log-file-path ()
132 | "Return path to the log file."
133 | (format nil "~amathb.log" *log-directory*))
134 |
135 | (defun write-log (fmt &rest args)
136 | "Log message with specified arguments."
137 | (when *log-mode*
138 | (append-file (log-file-path)
139 | (with-output-to-string (s)
140 | (format s "~a - [~a] \"~a ~a\" "
141 | (real-ip)
142 | (current-utc-time-string)
143 | (hunchentoot:request-method*)
144 | (hunchentoot:request-uri*))
145 | (apply #'format s fmt args)
146 | (terpri s)))))
147 |
148 | (defun lock (directory)
149 | "Acquire lock for specified directory and return t iff successful."
150 | (let ((lock-dir (merge-pathnames "lock/" directory))
151 | (failures 0)
152 | (max-failures 10)
153 | (status))
154 | (loop
155 | (when (nth-value 1 (ensure-directories-exist lock-dir))
156 | (write-log "Acquired lock")
157 | (setf status t)
158 | (return))
159 | (incf failures)
160 | (write-log "Could not acquire lock ~a after ~a of ~a attempts"
161 | lock-dir failures max-failures)
162 | (when (= failures max-failures)
163 | (write-log "Failed to acquire lock")
164 | (return))
165 | (sleep 0.2))
166 | status))
167 |
168 | (defun unlock (directory)
169 | "Release lock for specified directory."
170 | (uiop:delete-empty-directory (merge-pathnames "lock/" directory))
171 | (write-log "Released lock"))
172 |
173 | (defun read-options (directory)
174 | "Read options file."
175 | (let ((path (merge-pathnames "opt.lisp" directory)))
176 | (when (probe-file path)
177 | (read-from-string (read-file path)))))
178 |
179 | (defun from-post (name)
180 | "Get the value of a POST parameter."
181 | (hunchentoot:post-parameter name))
182 |
183 | (defun home-request-p (request)
184 | "Return true iff the home page is requested."
185 | (and (member (hunchentoot:request-method request) '(:head :get))
186 | (string= (hunchentoot:script-name request) "/")))
187 |
188 | (defun meta-request-p (request)
189 | "Return true iff the meta page is requested."
190 | (and (member (hunchentoot:request-method request) '(:head :get))
191 | (string= (hunchentoot:script-name request) "/0")))
192 |
193 | (defun math-request-p (request)
194 | "Return true iff a mathematics post is requested."
195 | (and (member (hunchentoot:request-method request) '(:head :get))
196 | (string/= (hunchentoot:script-name request) "/")
197 | (string/= (hunchentoot:script-name request) "/0")
198 | (every #'digit-char-p (subseq (hunchentoot:script-name request) 1))))
199 |
200 | (defun post-request-p (request)
201 | "Return true iff a post submission has been made."
202 | (and (member (hunchentoot:request-method request) '(:post))
203 | (or (string= (hunchentoot:script-name request) "/")
204 | (every #'digit-char-p (subseq (hunchentoot:script-name request) 1)))))
205 |
206 | (defun slug-to-path (directory slug)
207 | "Convert a slug to path, e.g., 1234567 to /directory/post/1/1234/1234567.txt"
208 | (let* ((short-prefix (floor slug 1000000))
209 | (long-prefix (floor slug 1000)))
210 | (format nil "~apost/~d/~d/~d.txt" directory short-prefix long-prefix slug)))
211 |
212 | (defun split-text (text)
213 | "Split text into head and body."
214 | (let ((delimiter-index (search (format nil "~%~%") text)))
215 | (values (subseq text 0 (1+ delimiter-index))
216 | (subseq text (+ 2 delimiter-index)))))
217 |
218 | (defun parse-headers (head)
219 | "Parse head content into an alist of key-value pairs."
220 | (let ((next-index 0)
221 | (headers))
222 | (loop
223 | (let* ((sep-index (search ":" head :start2 next-index))
224 | (end-index (search (format nil "~%") head :start2 sep-index))
225 | (key (subseq head next-index sep-index))
226 | (tail (subseq head (1+ sep-index) end-index))
227 | (value (if (string= tail "") "" (subseq tail 1))))
228 | (push (cons key value) headers)
229 | (setf next-index (1+ end-index))
230 | (when (= next-index (length head))
231 | (return))))
232 | headers))
233 |
234 | (defun parse-text (text)
235 | "Parse text into an alist of headers and body."
236 | (multiple-value-bind (head body) (split-text text)
237 | (let ((headers (parse-headers head)))
238 | (values (alist-get "Date" headers)
239 | (alist-get "Title" headers)
240 | (alist-get "Name" headers)
241 | body))))
242 |
243 | (defun read-slug (directory)
244 | "Read slug if it exists."
245 | (let ((filename (merge-pathnames "slug.txt" directory)))
246 | (when (probe-file filename)
247 | (parse-integer (read-file filename)))))
248 |
249 | (defun valid-slug (new-slug protected-slug)
250 | "Check if new slug is protected."
251 | (if (<= new-slug protected-slug)
252 | (write-log "Slug ~a is protected" new-slug)
253 | t))
254 |
255 | (defun increment-slug (directory protected-slug)
256 | "Acquire data lock, increment slug, and return t iff successful."
257 | (let ((filename (merge-pathnames "slug.txt" directory))
258 | (slug 0))
259 | (when (lock directory)
260 | (when (probe-file filename)
261 | (setf slug (parse-integer (read-file filename))))
262 | (incf slug)
263 | (when (<= slug protected-slug)
264 | (write-log "Cannot use protected slug ~a" slug)
265 | (setf slug nil))
266 | (when slug
267 | (write-file filename (format nil "~a~%" slug)))
268 | (unlock directory)
269 | slug)))
270 |
271 | (defun meta-code (directory last-post-time flood-table)
272 | "Return post code for meta page."
273 | (let ((slug (read-slug directory))
274 | (options (read-options directory)))
275 | (format nil "- Current time: ~a
276 | - Last post time: ~a
277 | - Last post slug: [~a](~a)
278 | - Flood table size: ~a
279 | - Options: ~a"
280 | (current-utc-time-string)
281 | (universal-time-string last-post-time)
282 | slug slug
283 | (hash-table-count flood-table)
284 | (floor (length options) 2))))
285 |
286 | (defun header-value (s)
287 | "Format header value to make is suitable for writing to text file."
288 | (if (string= s "") "" (format nil " ~a" s)))
289 |
290 | (defun make-text (date title name code)
291 | "Convert post fields to text to be written to post file."
292 | (format nil "Date:~a~%Title:~a~%Name:~a~%~%~a~%"
293 | (header-value date) (header-value title) (header-value name) code))
294 |
295 | (defun render-html (html date title name code error class)
296 | "Render HTML for a page."
297 | (setf html (string-replace "{{ date }}" date html))
298 | (setf html (string-replace "{{ title }}" title html))
299 | (setf html (string-replace "{{ name }}" name html))
300 | (setf html (string-replace "{{ code }}" code html))
301 | (setf html (string-replace "{{ error }}" error html))
302 | (setf html (string-replace "{{ class }}" class html)))
303 |
304 | (defun error-html (reason)
305 | "Return HTML for an error message."
306 | (format nil "