1094 lines
28 KiB
Go
1094 lines
28 KiB
Go
package server
|
|
|
|
import (
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"fes/modules/config"
|
|
)
|
|
|
|
func resetServeMux() {
|
|
http.DefaultServeMux = http.NewServeMux()
|
|
}
|
|
|
|
func chdir(t *testing.T, dir string) func() {
|
|
cwd, err := os.Getwd()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Chdir(dir); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return func() {
|
|
os.Chdir(cwd)
|
|
}
|
|
}
|
|
|
|
func freePort(t *testing.T) int {
|
|
l, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer l.Close()
|
|
return l.Addr().(*net.TCPAddr).Port
|
|
}
|
|
|
|
func startServer(t *testing.T, dir string) (*http.Server, int) {
|
|
resetServeMux()
|
|
|
|
restore := chdir(t, dir)
|
|
t.Cleanup(restore)
|
|
|
|
port := freePort(t)
|
|
config.Port = &port
|
|
|
|
srv, err := Start(".")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
t.Cleanup(func() {
|
|
Stop(srv)
|
|
})
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
return srv, port
|
|
}
|
|
|
|
func get(t *testing.T, port int, path string) (string, int) {
|
|
resp, err := http.Get("http://127.0.0.1:" + strconv.Itoa(port) + path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
b, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
return string(b), resp.StatusCode
|
|
}
|
|
|
|
type testCase struct {
|
|
name string
|
|
dir string
|
|
path string
|
|
expectBody string
|
|
expectEmpty bool
|
|
expectStatus int
|
|
expectHTMLHead bool
|
|
}
|
|
|
|
func TestServer(t *testing.T) {
|
|
cases := []testCase{
|
|
{
|
|
name: "lua basic",
|
|
dir: filepath.Join("..", "..", "test", "files", "lua-basic"),
|
|
path: "/",
|
|
expectBody: `<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🔥</text></svg>">
|
|
<title>Document</title>
|
|
<style>
|
|
:root {
|
|
--bg: #f5f5f5;
|
|
--text: #111827;
|
|
--muted: #6b7280;
|
|
--link: #1a0dab;
|
|
--accent: #68a6ff;
|
|
--highlight: #004d99;
|
|
--note-bg: #ffffff;
|
|
--panel-bg: #ffffff;
|
|
--border: rgba(0,0,0,.1);
|
|
--table-head: #f3f4f6;
|
|
--code-color: #004d99;
|
|
--blockquote-border: #1a73e8;
|
|
--banner-bg: #ffffff;
|
|
--footer-bg: #ffffff;
|
|
--shadow: rgba(0,0,0,.08);
|
|
}
|
|
|
|
@media (prefers-color-scheme: dark) {
|
|
:root {
|
|
--bg: #0f1113;
|
|
--text: #e6eef3;
|
|
--muted: #9aa6b1;
|
|
--link: #68a6ff;
|
|
--accent: #68a6ff;
|
|
--highlight: #cde7ff;
|
|
--note-bg: #1a1c20;
|
|
--panel-bg: #1a1c20;
|
|
--border: rgba(255,255,255,.06);
|
|
--table-head: #1a1c20;
|
|
--code-color: #cde7ff;
|
|
--blockquote-border: #68a6ff;
|
|
--banner-bg: #1a1c20;
|
|
--footer-bg: #1a1c20;
|
|
--shadow: rgba(0,0,0,.4);
|
|
}
|
|
}
|
|
|
|
html, body {
|
|
min-height: 100%;
|
|
margin: 0;
|
|
padding: 0;
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
line-height: 1.5;
|
|
-webkit-font-smoothing: antialiased;
|
|
-moz-osx-font-smoothing: grayscale;
|
|
}
|
|
|
|
body { padding: 36px; }
|
|
|
|
.container { max-width: 830px; margin: 0 auto; }
|
|
|
|
.container > *:not(.banner) { margin: 28px 0; }
|
|
|
|
h1, h2, h3, h4, h5, h6 { font-weight: 600; margin: 0 0 12px 0; }
|
|
|
|
h1 { font-size: 40px; margin-bottom: 20px; font-weight: 700; }
|
|
|
|
h2 { font-size: 32px; margin: 26px 0 14px; }
|
|
|
|
h3 { font-size: 26px; margin: 22px 0 12px; }
|
|
|
|
h4 { font-size: 20px; margin: 18px 0 10px; }
|
|
|
|
h5 { font-size: 16px; margin: 16px 0 8px; }
|
|
|
|
h6 { font-size: 14px; margin: 14px 0 6px; color: var(--muted); }
|
|
|
|
p { margin: 14px 0; }
|
|
|
|
a { color: var(--link); text-decoration: none; transition: color .15s ease, text-decoration-color .15s ease; }
|
|
|
|
.hidden { color: var(--text); text-decoration: none; }
|
|
|
|
a:hover { text-decoration: underline; }
|
|
|
|
summary { cursor: pointer; }
|
|
|
|
details {
|
|
background: var(--panel-bg);
|
|
border: 1px solid var(--border);
|
|
border-radius: 4px;
|
|
padding: 14px 16px;
|
|
margin: 16px 0;
|
|
}
|
|
|
|
details summary {
|
|
list-style: none;
|
|
font-weight: 600;
|
|
color: var(--text);
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
details summary::-webkit-details-marker { display: none; }
|
|
|
|
details summary::before {
|
|
content: "▸";
|
|
margin-right: 8px;
|
|
transition: transform .15s ease;
|
|
color: var(--accent);
|
|
}
|
|
|
|
details[open] summary::before { transform: rotate(90deg); }
|
|
|
|
summary::after { content: "Expand"; margin-left: auto; font-size: 13px; color: var(--muted); }
|
|
|
|
details[open] summary::after { content: "Collapse"; }
|
|
|
|
details > *:not(summary) { margin-top: 12px; }
|
|
|
|
.note, pre, code {
|
|
background: var(--note-bg);
|
|
border: 1px solid var(--border);
|
|
}
|
|
|
|
.note {
|
|
padding: 20px;
|
|
border-radius: 4px;
|
|
background: var(--note-bg);
|
|
border: 1px solid var(--border);
|
|
margin: 28px 0;
|
|
color: var(--text);
|
|
}
|
|
|
|
.note strong { color: var(--text); }
|
|
|
|
.muted { color: var(--muted); }
|
|
|
|
.lead { font-size: 15px; margin-top: 8px; }
|
|
|
|
.callout { display: block; margin: 12px 0; }
|
|
|
|
.small { font-size: 13px; color: var(--muted); margin-top: 6px; }
|
|
|
|
.highlight { font-weight: 700; color: var(--highlight); }
|
|
|
|
ul, ol { margin: 14px 0; padding-left: 26px; }
|
|
|
|
.tl {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, 200px);
|
|
gap: 15px;
|
|
list-style-type: none;
|
|
padding: 0;
|
|
margin: 0;
|
|
justify-content: start;
|
|
}
|
|
|
|
ul.tl li { padding: 10px; width: fit-content; }
|
|
|
|
li { margin: 6px 0; }
|
|
|
|
code {
|
|
padding: 3px 7px;
|
|
border-radius: 3px;
|
|
font-family: "SF Mono", Monaco, "Cascadia Code", "Roboto Mono", Consolas, "Courier New", monospace;
|
|
font-size: .9em;
|
|
color: var(--code-color);
|
|
}
|
|
|
|
pre {
|
|
padding: 20px;
|
|
border-radius: 4px;
|
|
margin: 14px 0;
|
|
overflow-x: auto;
|
|
font-family: "SF Mono", Monaco, "Cascadia Code", "Roboto Mono", Consolas, "Courier New", monospace;
|
|
font-size: 14px;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
pre code { background: none; border: none; padding: 0; font-size: inherit; }
|
|
|
|
blockquote {
|
|
border-left: 3px solid var(--blockquote-border);
|
|
padding-left: 18px;
|
|
margin: 14px 0;
|
|
color: var(--text);
|
|
font-style: italic;
|
|
}
|
|
|
|
hr { border: 0; border-top: 1px solid rgba(0,0,0,.08); margin: 26px 0; }
|
|
|
|
@media (prefers-color-scheme: dark) {
|
|
hr { border-top-color: rgba(255,255,255,.1); }
|
|
}
|
|
|
|
img { max-width: 100%; height: auto; border-radius: 4px; margin: 14px 0; }
|
|
|
|
table { width: 100%; border-collapse: collapse; margin: 14px 0; }
|
|
|
|
th, td {
|
|
padding: 12px 16px;
|
|
text-align: left;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
|
|
th {
|
|
background: var(--table-head);
|
|
font-weight: 600;
|
|
color: var(--text);
|
|
}
|
|
|
|
tr:hover { background: rgba(0,0,0,0.02); }
|
|
|
|
@media (prefers-color-scheme: dark) {
|
|
tr:hover { background: rgba(255,255,255,0.02); }
|
|
}
|
|
|
|
.divider { margin: 26px 0; height: 1px; background: rgba(0,0,0,.08); }
|
|
|
|
@media (prefers-color-scheme: dark) {
|
|
.divider { background: rgba(255,255,255,.1); }
|
|
}
|
|
|
|
.section { margin-top: 36px; }
|
|
|
|
.links { margin: 12px 0; }
|
|
|
|
.links a { display: inline-block; margin: 0 14px 6px 0; color: var(--link); }
|
|
|
|
strong, b { font-weight: 600; color: var(--text); }
|
|
|
|
em, i { font-style: italic; }
|
|
|
|
.center { display: flex; justify-content: center; align-items: center; }
|
|
|
|
.banner {
|
|
width: 100%;
|
|
box-sizing: border-box;
|
|
text-align: center;
|
|
background: var(--banner-bg);
|
|
padding: 20px;
|
|
border: 1px solid var(--border);
|
|
border-bottom-right-radius: 8px;
|
|
border-bottom-left-radius: 8px;
|
|
color: var(--text);
|
|
margin: -36px 0 28px 0;
|
|
box-shadow: 0 0.2em 0.6em var(--shadow);
|
|
}
|
|
|
|
.nav { margin-left: auto; margin-right: auto; }
|
|
|
|
.nav a { color: var(--highlight); }
|
|
|
|
.footer {
|
|
background: var(--footer-bg);
|
|
padding: 20px 0;
|
|
border-top: 1px solid rgba(0,0,0,.08);
|
|
font-size: 14px;
|
|
color: var(--muted);
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
gap: 24px;
|
|
margin-top: 28px !important;
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.left { text-align: left; float: left; }
|
|
|
|
.right { text-align: right; float: right; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<h1>Hello, World!</h1><footer class="footer">
|
|
<a href="https://git.vxserver.dev/fSD/fes" target="_blank">Fes Powered</a>
|
|
<a href="https://www.lua.org/" target="_blank">Lua Powered</a>
|
|
<a href="https://git.vxserver.dev/fSD/fes/src/branch/master/COPYING" target="_blank">ISC Licensed</a>
|
|
<p>© The Copyright Holder</p>
|
|
</footer>
|
|
</div>
|
|
</body>
|
|
</html>`,
|
|
},
|
|
{
|
|
name: "lua include",
|
|
dir: filepath.Join("..", "..", "test", "files", "lua-include"),
|
|
path: "/",
|
|
expectBody: `<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🔥</text></svg>">
|
|
<title>Document</title>
|
|
<style>
|
|
:root {
|
|
--bg: #f5f5f5;
|
|
--text: #111827;
|
|
--muted: #6b7280;
|
|
--link: #1a0dab;
|
|
--accent: #68a6ff;
|
|
--highlight: #004d99;
|
|
--note-bg: #ffffff;
|
|
--panel-bg: #ffffff;
|
|
--border: rgba(0,0,0,.1);
|
|
--table-head: #f3f4f6;
|
|
--code-color: #004d99;
|
|
--blockquote-border: #1a73e8;
|
|
--banner-bg: #ffffff;
|
|
--footer-bg: #ffffff;
|
|
--shadow: rgba(0,0,0,.08);
|
|
}
|
|
|
|
@media (prefers-color-scheme: dark) {
|
|
:root {
|
|
--bg: #0f1113;
|
|
--text: #e6eef3;
|
|
--muted: #9aa6b1;
|
|
--link: #68a6ff;
|
|
--accent: #68a6ff;
|
|
--highlight: #cde7ff;
|
|
--note-bg: #1a1c20;
|
|
--panel-bg: #1a1c20;
|
|
--border: rgba(255,255,255,.06);
|
|
--table-head: #1a1c20;
|
|
--code-color: #cde7ff;
|
|
--blockquote-border: #68a6ff;
|
|
--banner-bg: #1a1c20;
|
|
--footer-bg: #1a1c20;
|
|
--shadow: rgba(0,0,0,.4);
|
|
}
|
|
}
|
|
|
|
html, body {
|
|
min-height: 100%;
|
|
margin: 0;
|
|
padding: 0;
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
line-height: 1.5;
|
|
-webkit-font-smoothing: antialiased;
|
|
-moz-osx-font-smoothing: grayscale;
|
|
}
|
|
|
|
body { padding: 36px; }
|
|
|
|
.container { max-width: 830px; margin: 0 auto; }
|
|
|
|
.container > *:not(.banner) { margin: 28px 0; }
|
|
|
|
h1, h2, h3, h4, h5, h6 { font-weight: 600; margin: 0 0 12px 0; }
|
|
|
|
h1 { font-size: 40px; margin-bottom: 20px; font-weight: 700; }
|
|
|
|
h2 { font-size: 32px; margin: 26px 0 14px; }
|
|
|
|
h3 { font-size: 26px; margin: 22px 0 12px; }
|
|
|
|
h4 { font-size: 20px; margin: 18px 0 10px; }
|
|
|
|
h5 { font-size: 16px; margin: 16px 0 8px; }
|
|
|
|
h6 { font-size: 14px; margin: 14px 0 6px; color: var(--muted); }
|
|
|
|
p { margin: 14px 0; }
|
|
|
|
a { color: var(--link); text-decoration: none; transition: color .15s ease, text-decoration-color .15s ease; }
|
|
|
|
.hidden { color: var(--text); text-decoration: none; }
|
|
|
|
a:hover { text-decoration: underline; }
|
|
|
|
summary { cursor: pointer; }
|
|
|
|
details {
|
|
background: var(--panel-bg);
|
|
border: 1px solid var(--border);
|
|
border-radius: 4px;
|
|
padding: 14px 16px;
|
|
margin: 16px 0;
|
|
}
|
|
|
|
details summary {
|
|
list-style: none;
|
|
font-weight: 600;
|
|
color: var(--text);
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
details summary::-webkit-details-marker { display: none; }
|
|
|
|
details summary::before {
|
|
content: "▸";
|
|
margin-right: 8px;
|
|
transition: transform .15s ease;
|
|
color: var(--accent);
|
|
}
|
|
|
|
details[open] summary::before { transform: rotate(90deg); }
|
|
|
|
summary::after { content: "Expand"; margin-left: auto; font-size: 13px; color: var(--muted); }
|
|
|
|
details[open] summary::after { content: "Collapse"; }
|
|
|
|
details > *:not(summary) { margin-top: 12px; }
|
|
|
|
.note, pre, code {
|
|
background: var(--note-bg);
|
|
border: 1px solid var(--border);
|
|
}
|
|
|
|
.note {
|
|
padding: 20px;
|
|
border-radius: 4px;
|
|
background: var(--note-bg);
|
|
border: 1px solid var(--border);
|
|
margin: 28px 0;
|
|
color: var(--text);
|
|
}
|
|
|
|
.note strong { color: var(--text); }
|
|
|
|
.muted { color: var(--muted); }
|
|
|
|
.lead { font-size: 15px; margin-top: 8px; }
|
|
|
|
.callout { display: block; margin: 12px 0; }
|
|
|
|
.small { font-size: 13px; color: var(--muted); margin-top: 6px; }
|
|
|
|
.highlight { font-weight: 700; color: var(--highlight); }
|
|
|
|
ul, ol { margin: 14px 0; padding-left: 26px; }
|
|
|
|
.tl {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, 200px);
|
|
gap: 15px;
|
|
list-style-type: none;
|
|
padding: 0;
|
|
margin: 0;
|
|
justify-content: start;
|
|
}
|
|
|
|
ul.tl li { padding: 10px; width: fit-content; }
|
|
|
|
li { margin: 6px 0; }
|
|
|
|
code {
|
|
padding: 3px 7px;
|
|
border-radius: 3px;
|
|
font-family: "SF Mono", Monaco, "Cascadia Code", "Roboto Mono", Consolas, "Courier New", monospace;
|
|
font-size: .9em;
|
|
color: var(--code-color);
|
|
}
|
|
|
|
pre {
|
|
padding: 20px;
|
|
border-radius: 4px;
|
|
margin: 14px 0;
|
|
overflow-x: auto;
|
|
font-family: "SF Mono", Monaco, "Cascadia Code", "Roboto Mono", Consolas, "Courier New", monospace;
|
|
font-size: 14px;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
pre code { background: none; border: none; padding: 0; font-size: inherit; }
|
|
|
|
blockquote {
|
|
border-left: 3px solid var(--blockquote-border);
|
|
padding-left: 18px;
|
|
margin: 14px 0;
|
|
color: var(--text);
|
|
font-style: italic;
|
|
}
|
|
|
|
hr { border: 0; border-top: 1px solid rgba(0,0,0,.08); margin: 26px 0; }
|
|
|
|
@media (prefers-color-scheme: dark) {
|
|
hr { border-top-color: rgba(255,255,255,.1); }
|
|
}
|
|
|
|
img { max-width: 100%; height: auto; border-radius: 4px; margin: 14px 0; }
|
|
|
|
table { width: 100%; border-collapse: collapse; margin: 14px 0; }
|
|
|
|
th, td {
|
|
padding: 12px 16px;
|
|
text-align: left;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
|
|
th {
|
|
background: var(--table-head);
|
|
font-weight: 600;
|
|
color: var(--text);
|
|
}
|
|
|
|
tr:hover { background: rgba(0,0,0,0.02); }
|
|
|
|
@media (prefers-color-scheme: dark) {
|
|
tr:hover { background: rgba(255,255,255,0.02); }
|
|
}
|
|
|
|
.divider { margin: 26px 0; height: 1px; background: rgba(0,0,0,.08); }
|
|
|
|
@media (prefers-color-scheme: dark) {
|
|
.divider { background: rgba(255,255,255,.1); }
|
|
}
|
|
|
|
.section { margin-top: 36px; }
|
|
|
|
.links { margin: 12px 0; }
|
|
|
|
.links a { display: inline-block; margin: 0 14px 6px 0; color: var(--link); }
|
|
|
|
strong, b { font-weight: 600; color: var(--text); }
|
|
|
|
em, i { font-style: italic; }
|
|
|
|
.center { display: flex; justify-content: center; align-items: center; }
|
|
|
|
.banner {
|
|
width: 100%;
|
|
box-sizing: border-box;
|
|
text-align: center;
|
|
background: var(--banner-bg);
|
|
padding: 20px;
|
|
border: 1px solid var(--border);
|
|
border-bottom-right-radius: 8px;
|
|
border-bottom-left-radius: 8px;
|
|
color: var(--text);
|
|
margin: -36px 0 28px 0;
|
|
box-shadow: 0 0.2em 0.6em var(--shadow);
|
|
}
|
|
|
|
.nav { margin-left: auto; margin-right: auto; }
|
|
|
|
.nav a { color: var(--highlight); }
|
|
|
|
.footer {
|
|
background: var(--footer-bg);
|
|
padding: 20px 0;
|
|
border-top: 1px solid rgba(0,0,0,.08);
|
|
font-size: 14px;
|
|
color: var(--muted);
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
gap: 24px;
|
|
margin-top: 28px !important;
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.left { text-align: left; float: left; }
|
|
|
|
.right { text-align: right; float: right; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<h1>Hello, World!</h1>
|
|
<div class="note"><h2>Hello, World!</h2></div><footer class="footer">
|
|
<a href="https://git.vxserver.dev/fSD/fes" target="_blank">Fes Powered</a>
|
|
<a href="https://www.lua.org/" target="_blank">Lua Powered</a>
|
|
<a href="https://git.vxserver.dev/fSD/fes/src/branch/master/COPYING" target="_blank">ISC Licensed</a>
|
|
<p>© The Copyright Holder</p>
|
|
</footer>
|
|
</div>
|
|
</body>
|
|
</html>`,
|
|
},
|
|
{
|
|
name: "lua error",
|
|
dir: filepath.Join("..", "..", "test", "files", "lua-error"),
|
|
path: "/",
|
|
expectStatus: http.StatusInternalServerError,
|
|
},
|
|
{
|
|
name: "markdown",
|
|
dir: filepath.Join("..", "..", "test", "files", "markdown"),
|
|
path: "/",
|
|
expectBody: strings.ReplaceAll(`<style>body {max-width: 80ch;}</style>
|
|
<h1 id="markdown-syntax">Markdown: Syntax</h1>
|
|
|
|
<ul>
|
|
<li><a href="#overview">Overview</a>
|
|
|
|
<ul>
|
|
<li><a href="#philosophy">Philosophy</a></li>
|
|
<li><a href="#html">Inline HTML</a></li>
|
|
<li><a href="#autoescape">Automatic Escaping for Special Characters</a></li>
|
|
</ul></li>
|
|
<li><a href="#block">Block Elements</a>
|
|
|
|
<ul>
|
|
<li><a href="#p">Paragraphs and Line Breaks</a></li>
|
|
<li><a href="#header">Headers</a></li>
|
|
<li><a href="#blockquote">Blockquotes</a></li>
|
|
<li><a href="#list">Lists</a></li>
|
|
<li><a href="#precode">Code Blocks</a></li>
|
|
<li><a href="#hr">Horizontal Rules</a></li>
|
|
</ul></li>
|
|
<li><a href="#span">Span Elements</a>
|
|
|
|
<ul>
|
|
<li><a href="#link">Links</a></li>
|
|
<li><a href="#em">Emphasis</a></li>
|
|
<li><a href="#code">Code</a></li>
|
|
<li><a href="#img">Images</a></li>
|
|
</ul></li>
|
|
<li><a href="#misc">Miscellaneous</a>
|
|
|
|
<ul>
|
|
<li><a href="#backslash">Backslash Escapes</a></li>
|
|
<li><a href="#autolink">Automatic Links</a></li>
|
|
</ul></li>
|
|
</ul>
|
|
|
|
<p><strong>Note:</strong> This document is itself written using Markdown; you
|
|
can <a href="/projects/markdown/syntax.text">see the source for it by adding ‘.text’ to the URL</a>.</p>
|
|
|
|
<hr>
|
|
|
|
<h2 id="overview">Overview</h2>
|
|
|
|
<h3 id="philosophy">Philosophy</h3>
|
|
|
|
<p>Markdown is intended to be as easy-to-read and easy-to-write as is feasible.</p>
|
|
|
|
<p>Readability, however, is emphasized above all else. A Markdown-formatted
|
|
document should be publishable as-is, as plain text, without looking
|
|
like it’s been marked up with tags or formatting instructions. While
|
|
Markdown’s syntax has been influenced by several existing text-to-HTML
|
|
filters – including <a href="http://docutils.sourceforge.net/mirror/setext.html" target="_blank">Setext</a>, <a href="http://www.aaronsw.com/2002/atx/" target="_blank">atx</a>, <a href="http://textism.com/tools/textile/" target="_blank">Textile</a>, <a href="http://docutils.sourceforge.net/rst.html" target="_blank">reStructuredText</a>,
|
|
<a href="http://www.triptico.com/software/grutatxt.html" target="_blank">Grutatext</a>, and <a href="http://ettext.taint.org/doc/" target="_blank">EtText</a> – the single biggest source of
|
|
inspiration for Markdown’s syntax is the format of plain text email.</p>
|
|
|
|
<h2 id="block-elements">Block Elements</h2>
|
|
|
|
<h3 id="paragraphs-and-line-breaks">Paragraphs and Line Breaks</h3>
|
|
|
|
<p>A paragraph is simply one or more consecutive lines of text, separated
|
|
by one or more blank lines. (A blank line is any line that looks like a
|
|
blank line – a line containing nothing but spaces or tabs is considered
|
|
blank.) Normal paragraphs should not be indented with spaces or tabs.</p>
|
|
|
|
<p>The implication of the “one or more consecutive lines of text” rule is
|
|
that Markdown supports “hard-wrapped” text paragraphs. This differs
|
|
significantly from most other text-to-HTML formatters (including Movable
|
|
Type’s “Convert Line Breaks” option) which translate every line break
|
|
character in a paragraph into a <code><br /></code> tag.</p>
|
|
|
|
<p>When you <em>do</em> want to insert a <code><br /></code> break tag using Markdown, you
|
|
end a line with two or more spaces, then type return.</p>
|
|
|
|
<h3 id="headers">Headers</h3>
|
|
|
|
<p>Markdown supports two styles of headers, [Setext] [1] and [atx] [2].</p>
|
|
|
|
<p>Optionally, you may “close” atx-style headers. This is purely
|
|
cosmetic – you can use this if you think it looks better. The
|
|
closing hashes don’t even need to match the number of hashes
|
|
used to open the header. (The number of opening hashes
|
|
determines the header level.)</p>
|
|
|
|
<h3 id="blockquotes">Blockquotes</h3>
|
|
|
|
<p>Markdown uses email-style <code>></code> characters for blockquoting. If you’re
|
|
familiar with quoting passages of text in an email message, then you
|
|
know how to create a blockquote in Markdown. It looks best if you hard
|
|
wrap the text and put a <code>></code> before every line:</p>
|
|
|
|
<blockquote>
|
|
<p>This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet,
|
|
consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus.
|
|
Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus.</p>
|
|
|
|
<p>Donec sit amet nisl. Aliquam semper ipsum sit amet velit. Suspendisse
|
|
id sem consectetuer libero luctus adipiscing.</p>
|
|
</blockquote>
|
|
|
|
<p>Markdown allows you to be lazy and only put the <code>></code> before the first
|
|
line of a hard-wrapped paragraph:</p>
|
|
|
|
<blockquote>
|
|
<p>This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet,
|
|
consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus.
|
|
Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus.</p>
|
|
|
|
<p>Donec sit amet nisl. Aliquam semper ipsum sit amet velit. Suspendisse
|
|
id sem consectetuer libero luctus adipiscing.</p>
|
|
</blockquote>
|
|
|
|
<p>Blockquotes can be nested (i.e. a blockquote-in-a-blockquote) by
|
|
adding additional levels of <code>></code>:</p>
|
|
|
|
<blockquote>
|
|
<p>This is the first level of quoting.</p>
|
|
|
|
<blockquote>
|
|
<p>This is nested blockquote.</p>
|
|
</blockquote>
|
|
|
|
<p>Back to the first level.</p>
|
|
</blockquote>
|
|
|
|
<p>Blockquotes can contain other Markdown elements, including headers, lists,
|
|
and code blocks:</p>
|
|
|
|
<blockquote>
|
|
<h2 id="this-is-a-header">This is a header.</h2>
|
|
|
|
<ol>
|
|
<li>This is the first list item.</li>
|
|
<li>This is the second list item.</li>
|
|
</ol>
|
|
|
|
<p>Here’s some example code:</p>
|
|
|
|
<pre><code>return shell_exec("echo $input | $markdown_script");
|
|
</code></pre>
|
|
</blockquote>
|
|
|
|
<p>Any decent text editor should make email-style quoting easy. For
|
|
example, with BBEdit, you can make a selection and choose Increase
|
|
Quote Level from the Text menu.</p>
|
|
|
|
<h3 id="lists">Lists</h3>
|
|
|
|
<p>Markdown supports ordered (numbered) and unordered (bulleted) lists.</p>
|
|
|
|
<p>Unordered lists use asterisks, pluses, and hyphens – interchangably
|
|
– as list markers:</p>
|
|
|
|
<ul>
|
|
<li>Red</li>
|
|
<li>Green</li>
|
|
<li>Blue</li>
|
|
</ul>
|
|
|
|
<p>is equivalent to:</p>
|
|
|
|
<ul>
|
|
<li>Red</li>
|
|
<li>Green</li>
|
|
<li>Blue</li>
|
|
</ul>
|
|
|
|
<p>and:</p>
|
|
|
|
<ul>
|
|
<li>Red</li>
|
|
<li>Green</li>
|
|
<li>Blue</li>
|
|
</ul>
|
|
|
|
<p>Ordered lists use numbers followed by periods:</p>
|
|
|
|
<ol>
|
|
<li>Bird</li>
|
|
<li>McHale</li>
|
|
<li>Parish</li>
|
|
</ol>
|
|
|
|
<p>It’s important to note that the actual numbers you use to mark the
|
|
list have no effect on the HTML output Markdown produces. The HTML
|
|
Markdown produces from the above list is:</p>
|
|
|
|
<p>If you instead wrote the list in Markdown like this:</p>
|
|
|
|
<ol>
|
|
<li>Bird</li>
|
|
<li>McHale</li>
|
|
<li>Parish</li>
|
|
</ol>
|
|
|
|
<p>or even:</p>
|
|
|
|
<ol>
|
|
<li>Bird</li>
|
|
<li>McHale</li>
|
|
<li>Parish</li>
|
|
</ol>
|
|
|
|
<p>you’d get the exact same HTML output. The point is, if you want to,
|
|
you can use ordinal numbers in your ordered Markdown lists, so that
|
|
the numbers in your source match the numbers in your published HTML.
|
|
But if you want to be lazy, you don’t have to.</p>
|
|
|
|
<p>To make lists look nice, you can wrap items with hanging indents:</p>
|
|
|
|
<ul>
|
|
<li>Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
|
|
Aliquam hendrerit mi posuere lectus. Vestibulum enim wisi,
|
|
viverra nec, fringilla in, laoreet vitae, risus.</li>
|
|
<li>Donec sit amet nisl. Aliquam semper ipsum sit amet velit.
|
|
Suspendisse id sem consectetuer libero luctus adipiscing.</li>
|
|
</ul>
|
|
|
|
<p>But if you want to be lazy, you don’t have to:</p>
|
|
|
|
<ul>
|
|
<li>Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
|
|
Aliquam hendrerit mi posuere lectus. Vestibulum enim wisi,
|
|
viverra nec, fringilla in, laoreet vitae, risus.</li>
|
|
<li>Donec sit amet nisl. Aliquam semper ipsum sit amet velit.
|
|
Suspendisse id sem consectetuer libero luctus adipiscing.</li>
|
|
</ul>
|
|
|
|
<p>List items may consist of multiple paragraphs. Each subsequent
|
|
paragraph in a list item must be indented by either 4 spaces
|
|
or one tab:</p>
|
|
|
|
<ol>
|
|
<li><p>This is a list item with two paragraphs. Lorem ipsum dolor
|
|
sit amet, consectetuer adipiscing elit. Aliquam hendrerit
|
|
mi posuere lectus.</p>
|
|
|
|
<p>Vestibulum enim wisi, viverra nec, fringilla in, laoreet
|
|
vitae, risus. Donec sit amet nisl. Aliquam semper ipsum
|
|
sit amet velit.</p></li>
|
|
|
|
<li><p>Suspendisse id sem consectetuer libero luctus adipiscing.</p></li>
|
|
</ol>
|
|
|
|
<p>It looks nice if you indent every line of the subsequent
|
|
paragraphs, but here again, Markdown will allow you to be
|
|
lazy:</p>
|
|
|
|
<ul>
|
|
<li><p>This is a list item with two paragraphs.</p>
|
|
|
|
<p>This is the second paragraph in the list item. You’re
|
|
only required to indent the first line. Lorem ipsum dolor
|
|
sit amet, consectetuer adipiscing elit.</p></li>
|
|
|
|
<li><p>Another item in the same list.</p></li>
|
|
</ul>
|
|
|
|
<p>To put a blockquote within a list item, the blockquote’s <code>></code>
|
|
delimiters need to be indented:</p>
|
|
|
|
<ul>
|
|
<li><p>A list item with a blockquote:</p>
|
|
|
|
<blockquote>
|
|
<p>This is a blockquote
|
|
inside a list item.</p>
|
|
</blockquote>
|
|
</li>
|
|
</ul>
|
|
|
|
<p>To put a code block within a list item, the code block needs
|
|
to be indented <em>twice</em> – 8 spaces or two tabs:</p>
|
|
|
|
<ul>
|
|
<li><p>A list item with a code block:</p>
|
|
|
|
<pre><code><code goes here>
|
|
</code></pre></li>
|
|
</ul>
|
|
|
|
<h3 id="code-blocks">Code Blocks</h3>
|
|
|
|
<p>Pre-formatted code blocks are used for writing about programming or
|
|
markup source code. Rather than forming normal paragraphs, the lines
|
|
of a code block are interpreted literally. Markdown wraps a code block
|
|
in both <code><pre></code> and <code><code></code> tags.</p>
|
|
|
|
<p>To produce a code block in Markdown, simply indent every line of the
|
|
block by at least 4 spaces or 1 tab.</p>
|
|
|
|
<p>This is a normal paragraph:</p>
|
|
|
|
<pre><code>This is a code block.
|
|
</code></pre>
|
|
|
|
<p>Here is an example of AppleScript:</p>
|
|
|
|
<pre><code>tell application "Foo"
|
|
beep
|
|
end tell
|
|
</code></pre>
|
|
|
|
<p>A code block continues until it reaches a line that is not indented
|
|
(or the end of the article).</p>
|
|
|
|
<p>Within a code block, ampersands (<code>&</code>) and angle brackets (<code><</code> and <code>></code>)
|
|
are automatically converted into HTML entities. This makes it very
|
|
easy to include example HTML source code using Markdown – just paste
|
|
it and indent it, and Markdown will handle the hassle of encoding the
|
|
ampersands and angle brackets. For example, this:</p>
|
|
|
|
<pre><code><div class="footer">
|
|
&copy; 2004 Foo Corporation
|
|
</div>
|
|
</code></pre>
|
|
|
|
<p>Regular Markdown syntax is not processed within code blocks. E.g.,
|
|
asterisks are just literal asterisks within a code block. This means
|
|
it’s also easy to use Markdown to write about Markdown’s own syntax.</p>
|
|
|
|
<pre><code>tell application "Foo"
|
|
beep
|
|
end tell
|
|
</code></pre>
|
|
|
|
<h2 id="span-elements">Span Elements</h2>
|
|
|
|
<h3 id="links">Links</h3>
|
|
|
|
<p>Markdown supports two style of links: <em>inline</em> and <em>reference</em>.</p>
|
|
|
|
<p>In both styles, the link text is delimited by [square brackets].</p>
|
|
|
|
<p>To create an inline link, use a set of regular parentheses immediately
|
|
after the link text’s closing square bracket. Inside the parentheses,
|
|
put the URL where you want the link to point, along with an <em>optional</em>
|
|
title for the link, surrounded in quotes. For example:</p>
|
|
|
|
<p>This is <a href="http://example.com/" target="_blank">an example</a> inline link.</p>
|
|
|
|
<p><a href="http://example.net/" target="_blank">This link</a> has no title attribute.</p>
|
|
|
|
<h3 id="emphasis">Emphasis</h3>
|
|
|
|
<p>Markdown treats asterisks (<code>*</code>) and underscores (<code>_</code>) as indicators of
|
|
emphasis. Text wrapped with one <code>*</code> or <code>_</code> will be wrapped with an
|
|
HTML <code><em></code> tag; double <code>*</code>’s or <code>_</code>’s will be wrapped with an HTML
|
|
<code><strong></code> tag. E.g., this input:</p>
|
|
|
|
<p><em>single asterisks</em></p>
|
|
|
|
<p><em>single underscores</em></p>
|
|
|
|
<p><strong>double asterisks</strong></p>
|
|
|
|
<p><strong>double underscores</strong></p>
|
|
|
|
<h3 id="code">Code</h3>
|
|
|
|
<p>To indicate a span of code, wrap it with backtick quotes (<code>$$</code>).
|
|
Unlike a pre-formatted code block, a code span indicates code within a
|
|
normal paragraph. For example:</p>
|
|
|
|
<p>Use the <code>printf()</code> function.</p>`, "$$", "`"),
|
|
},
|
|
{
|
|
name: "archive index",
|
|
dir: filepath.Join("..", "..", "test", "files", "archive"),
|
|
path: "/archive/",
|
|
expectHTMLHead: true,
|
|
},
|
|
{
|
|
name: "not found",
|
|
dir: filepath.Join("..", "..", "test", "files", "minimal"),
|
|
path: "/does-not-exist",
|
|
expectStatus: http.StatusNotFound,
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
_, port := startServer(t, tc.dir)
|
|
|
|
body, status := get(t, port, tc.path)
|
|
|
|
if tc.expectStatus != 0 {
|
|
if status != tc.expectStatus {
|
|
t.Fatalf("expected %d, got %d", tc.expectStatus, status)
|
|
}
|
|
return
|
|
}
|
|
|
|
if tc.expectBody != "" {
|
|
if body != tc.expectBody {
|
|
t.Fatalf("unexpected body:\n--- got ---\n%s\n--- want ---\n%s", body, tc.expectBody)
|
|
}
|
|
return
|
|
}
|
|
|
|
if body == "" && !tc.expectEmpty {
|
|
t.Fatal("empty response")
|
|
}
|
|
|
|
if tc.expectHTMLHead && len(body) >= 6 && body[:6] != "<html>" {
|
|
t.Fatal("archive index is not html")
|
|
}
|
|
})
|
|
}
|
|
}
|