Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
c21f834054 |
|||
|
b87dc22f1c |
|||
|
4f86e2963c |
|||
|
7e4bbcef1b |
|||
|
2cf57bea64 |
7 changed files with 21 additions and 504 deletions
|
|
@ -1,21 +0,0 @@
|
||||||
ANTI-CAPITALIST SOFTWARE LICENSE (v 1.4)
|
|
||||||
|
|
||||||
Copyright © 2026 Thomasorus
|
|
||||||
|
|
||||||
This is anti-capitalist software, released for free use by individuals and organizations that do not operate by capitalist principles.
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person or organization (the "User") obtaining a copy of this software and associated documentation files (the "Software"), to use, copy, modify, merge, distribute, and/or sell copies of the Software, subject to the following conditions:
|
|
||||||
|
|
||||||
1. The above copyright notice and this permission notice shall be included in all copies or modified versions of the Software.
|
|
||||||
|
|
||||||
2. The User is one of the following:
|
|
||||||
a. An individual person, laboring for themselves
|
|
||||||
b. A non-profit organization
|
|
||||||
c. An educational institution
|
|
||||||
d. An organization that seeks shared profit for all of its members, and allows non-members to set the cost of their labor
|
|
||||||
|
|
||||||
3. If the User is an organization with owners, then all owners are workers and all workers are owners with equal equity and/or equal vote.
|
|
||||||
|
|
||||||
4. If the User is an organization, then the User is not law enforcement or military, or working for or under either.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT EXPRESS OR IMPLIED WARRANTY OF ANY KIND, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
|
|
@ -1,234 +0,0 @@
|
||||||
# Kaku 書く
|
|
||||||
|
|
||||||
Kaku (write) is my own markup language. It's inspired by Markdown with a few modification for quotes, links, images and lists handling, and more. It was created to fit my needs. This repositoty contains a parser for Kaku.
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
Kaku 書く is a free to use by individuals and organizations that do not operate by capitalist principles. For more information see the [license](LICENSE) file.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Headings
|
|
||||||
|
|
||||||
`#` through `######` for heading levels 1 through 6. Each heading gets an
|
|
||||||
auto-generated `id` and a clickable permalink anchor.
|
|
||||||
|
|
||||||
```
|
|
||||||
# Heading level 1
|
|
||||||
## Heading level 2
|
|
||||||
###### Heading level 6
|
|
||||||
```
|
|
||||||
|
|
||||||
Renders as:
|
|
||||||
```html
|
|
||||||
<h1 id="heading-level-1">Heading level 1<a href="#heading-level-1" ...>#</a></h1>
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Basic inline formatting
|
|
||||||
|
|
||||||
| Syntax | Result |
|
|
||||||
|---|---|
|
|
||||||
| `*bold*` | **bold** → `<strong>` |
|
|
||||||
| `_emphasis_` | *emphasis* → `<em>` |
|
|
||||||
| `~strike~` | ~~strike~~ → `<del>` |
|
|
||||||
| `` `code` `` | `code` → `<code>` |
|
|
||||||
|
|
||||||
```
|
|
||||||
This is *bold*, this is _emphasis_, this is ~strikethrough~, and this is `inline code`.
|
|
||||||
```
|
|
||||||
|
|
||||||
Inline formatting can be combined with links and images (see below).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Horizontal rule
|
|
||||||
|
|
||||||
Four dashes on their own line:
|
|
||||||
|
|
||||||
```
|
|
||||||
----
|
|
||||||
```
|
|
||||||
|
|
||||||
Renders as `<hr>`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Code blocks
|
|
||||||
|
|
||||||
Triple backticks, with an optional language right after the opening fence:
|
|
||||||
|
|
||||||
````
|
|
||||||
```go
|
|
||||||
func main() {
|
|
||||||
fmt.Println("hello")
|
|
||||||
}
|
|
||||||
```
|
|
||||||
````
|
|
||||||
|
|
||||||
Renders as:
|
|
||||||
```html
|
|
||||||
<pre><code class="language-go">func main() {
|
|
||||||
fmt.Println("hello")
|
|
||||||
}</code></pre>
|
|
||||||
```
|
|
||||||
|
|
||||||
Content inside a code block is never parsed for other Kaku syntax — everything
|
|
||||||
stays literal.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Lists
|
|
||||||
|
|
||||||
### Bullet list
|
|
||||||
```
|
|
||||||
- First item
|
|
||||||
- Second item
|
|
||||||
- Third item
|
|
||||||
```
|
|
||||||
|
|
||||||
### Ordered list
|
|
||||||
```
|
|
||||||
+ First item
|
|
||||||
+ Second item
|
|
||||||
+ Third item
|
|
||||||
```
|
|
||||||
|
|
||||||
### Definition list
|
|
||||||
```
|
|
||||||
? Term one : Its definition
|
|
||||||
? Term two : Its definition
|
|
||||||
```
|
|
||||||
|
|
||||||
List item text supports inline formatting (bold, links, etc.):
|
|
||||||
```
|
|
||||||
- Check out (link: https://example.com text: this link)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Links
|
|
||||||
|
|
||||||
```
|
|
||||||
(link: https://example.com text: Click here)
|
|
||||||
(link: https://example.com text: Click here title: Hover tooltip)
|
|
||||||
(link: https://example.com text: Click here label: Accessible label)
|
|
||||||
```
|
|
||||||
|
|
||||||
Fields: `link` (required), `text`, `label`, `title` — order doesn't matter,
|
|
||||||
all except `link` are optional.
|
|
||||||
|
|
||||||
Links also work **inline**, inside a paragraph or list item:
|
|
||||||
```
|
|
||||||
Check out (link: https://example.com text: this) for more info.
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Images
|
|
||||||
|
|
||||||
```
|
|
||||||
(image: photo.jpg alt: A description of the photo)
|
|
||||||
(image: photo.jpg alt: A description figcaption: An optional caption)
|
|
||||||
```
|
|
||||||
|
|
||||||
Fields: `image` (required), `alt`, `figcaption`.
|
|
||||||
|
|
||||||
Images work inline too, and can be made clickable by nesting inside a link:
|
|
||||||
```
|
|
||||||
(link: https://example.com text: (image: photo.jpg alt: A photo))
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Video
|
|
||||||
|
|
||||||
```
|
|
||||||
(video: clip.mp4)
|
|
||||||
(video: clip.mp4 autoplay)
|
|
||||||
```
|
|
||||||
|
|
||||||
Plain video gets standard playback controls. Adding `autoplay` makes it behave
|
|
||||||
like a looping, muted, GIF-style clip instead (`autoplay loop muted playsinline`).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Audio
|
|
||||||
|
|
||||||
```
|
|
||||||
(audio: track.mp3)
|
|
||||||
```
|
|
||||||
|
|
||||||
Renders as an `<audio>` element with standard controls.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Quotes
|
|
||||||
|
|
||||||
```
|
|
||||||
(quote: A quote can stand on its own.)
|
|
||||||
(quote: With an author. author: Jane Doe)
|
|
||||||
(quote: With a source too. author: Jane Doe source: Her Book)
|
|
||||||
(quote: With a link as well. author: Jane Doe source: Her Book link: https://example.com)
|
|
||||||
```
|
|
||||||
|
|
||||||
Fields: `quote` (required), `author`, `source`, `link` — all optional except
|
|
||||||
`quote`. Renders as a `<figure>` with a `<blockquote>` and, if any of
|
|
||||||
`author`/`source`/`link` are given, a `<figcaption>`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Asides
|
|
||||||
|
|
||||||
A line wrapped entirely in curly braces becomes a side note:
|
|
||||||
|
|
||||||
```
|
|
||||||
{This is a side note, separate from the main flow of the text.}
|
|
||||||
```
|
|
||||||
|
|
||||||
Renders as:
|
|
||||||
```html
|
|
||||||
<aside><p>Side note</p>This is a side note, separate from the main flow of the text.</aside>
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Details / collapsible sections
|
|
||||||
|
|
||||||
```
|
|
||||||
(details: The hidden content goes here. summary: Click to expand)
|
|
||||||
```
|
|
||||||
|
|
||||||
Fields: `details` (the hidden content), `summary` (the always-visible label).
|
|
||||||
Renders as a native `<details>`/`<summary>` block.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Todo lists
|
|
||||||
|
|
||||||
```
|
|
||||||
[ ] Not started
|
|
||||||
[@] In progress
|
|
||||||
[~] Paused or abandoned
|
|
||||||
[x] Completed
|
|
||||||
```
|
|
||||||
|
|
||||||
Consecutive todo lines are grouped into a single list. Each state renders
|
|
||||||
with a visually-hidden accessible label (`Opened task`, `Ongoing task`,
|
|
||||||
`Paused or abandonned task`, `Completed task`).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Paragraphs
|
|
||||||
|
|
||||||
Any line that isn't one of the above is treated as regular paragraph text.
|
|
||||||
Consecutive lines are joined into a single paragraph; a blank line starts a
|
|
||||||
new one.
|
|
||||||
|
|
||||||
```
|
|
||||||
This is the first line of a paragraph.
|
|
||||||
This is the second line, joined to the first.
|
|
||||||
|
|
||||||
This is a new paragraph.
|
|
||||||
```
|
|
||||||
|
|
@ -31,7 +31,6 @@ func parseInline(text string) []Node {
|
||||||
for i := 0; i < len(runes); {
|
for i := 0; i < len(runes); {
|
||||||
switch runes[i] {
|
switch runes[i] {
|
||||||
case '`':
|
case '`':
|
||||||
// Look ahead for matching backtick and if it exists creates a code node
|
|
||||||
if end := findNextRune(runes, i+1, '`'); end != -1 {
|
if end := findNextRune(runes, i+1, '`'); end != -1 {
|
||||||
flush()
|
flush()
|
||||||
nodes = append(nodes, &Code{Value: string(runes[i+1 : end])})
|
nodes = append(nodes, &Code{Value: string(runes[i+1 : end])})
|
||||||
|
|
@ -51,7 +50,7 @@ func parseInline(text string) []Node {
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// Checks if the rune is part of delimeters, if yes look ahead for matching tag and if it exists creates a code node
|
// Checks if the rune is part of delimeters, if yes look ahead for matching tag and if it exists creates a node
|
||||||
if d, ok := checkDelimeter(runes[i]); ok {
|
if d, ok := checkDelimeter(runes[i]); ok {
|
||||||
if end := findNextRune(runes, i+1, d.rune); end != -1 {
|
if end := findNextRune(runes, i+1, d.rune); end != -1 {
|
||||||
flush()
|
flush()
|
||||||
|
|
@ -80,7 +79,7 @@ func checkDelimeter(r rune) (delimiter, bool) {
|
||||||
return delimiter{}, false
|
return delimiter{}, false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finds the position of the next occurrence of a given rune, or -1 if none.
|
// Find the position of the next occurrence of a given rune, or -1 if none.
|
||||||
func findNextRune(runes []rune, from int, delim rune) int {
|
func findNextRune(runes []rune, from int, delim rune) int {
|
||||||
for i := from; i < len(runes); i++ {
|
for i := from; i < len(runes); i++ {
|
||||||
if runes[i] == delim {
|
if runes[i] == delim {
|
||||||
|
|
|
||||||
|
|
@ -63,8 +63,7 @@ func hasParenthesisPrefix(text string, prefixes []string) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// isTodoLine reports whether the line matches "[X] text" where X is one
|
// isTodoLine reports whether the line matches "[X] text" where X is one of the known todo state characters.
|
||||||
// of the known todo state characters.
|
|
||||||
func isTodoLine(trimmed string) bool {
|
func isTodoLine(trimmed string) bool {
|
||||||
if len(trimmed) < 3 || trimmed[0] != '[' || trimmed[2] != ']' {
|
if len(trimmed) < 3 || trimmed[0] != '[' || trimmed[2] != ']' {
|
||||||
return false
|
return false
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ func (p *parser) advance() {
|
||||||
p.pos++
|
p.pos++
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse turns raw Kaku source into a Document.
|
// Parse turns raw Kaku source into a Document, which has Node as children.
|
||||||
func Parse(input string) (doc *Document) {
|
func Parse(input string) (doc *Document) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
|
|
@ -71,7 +71,7 @@ func Parse(input string) (doc *Document) {
|
||||||
return &Document{Children: children}
|
return &Document{Children: children}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns Heading struct
|
// Returns Heading node
|
||||||
func parseHeading(line string) *Heading {
|
func parseHeading(line string) *Heading {
|
||||||
trimmed := strings.TrimSpace(line)
|
trimmed := strings.TrimSpace(line)
|
||||||
level := len(trimmed) - len(strings.TrimLeft(trimmed, "#"))
|
level := len(trimmed) - len(strings.TrimLeft(trimmed, "#"))
|
||||||
|
|
@ -79,9 +79,7 @@ func parseHeading(line string) *Heading {
|
||||||
return &Heading{Level: level, Children: parseInline(text)}
|
return &Heading{Level: level, Children: parseInline(text)}
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseCodeBlock consumes lines from the opening ``` fence to the closing
|
// Returns CodeBlock node and doesn't parse text between delimeters
|
||||||
// ``` fence, and returns a CodeBlock with the language (if given on the
|
|
||||||
// opening fence) and the raw, un-parsed content between them.
|
|
||||||
func parseCodeBlock(p *parser) *CodeBlock {
|
func parseCodeBlock(p *parser) *CodeBlock {
|
||||||
const fence = "```"
|
const fence = "```"
|
||||||
lang := strings.TrimPrefix(strings.TrimSpace(p.current()), fence)
|
lang := strings.TrimPrefix(strings.TrimSpace(p.current()), fence)
|
||||||
|
|
@ -102,8 +100,7 @@ func parseCodeBlock(p *parser) *CodeBlock {
|
||||||
return &CodeBlock{Language: lang, Content: strings.Join(content, "\n")}
|
return &CodeBlock{Language: lang, Content: strings.Join(content, "\n")}
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseParagraph joins consecutive plain lines into one Paragraph, until
|
// Joins consecutive plain lines into one paragraph node until a blank line or a different kind of block starts.
|
||||||
// a blank line or a different kind of block starts.
|
|
||||||
func parseParagraph(p *parser) *Paragraph {
|
func parseParagraph(p *parser) *Paragraph {
|
||||||
var text []string
|
var text []string
|
||||||
for !p.done() {
|
for !p.done() {
|
||||||
|
|
@ -116,9 +113,7 @@ func parseParagraph(p *parser) *Paragraph {
|
||||||
return &Paragraph{Children: parseInline(strings.Join(text, " "))}
|
return &Paragraph{Children: parseInline(strings.Join(text, " "))}
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseList groups consecutive lines starting with prefix into a List of
|
// Groups consecutive lines starting with - or + as List node and run content through parseInline
|
||||||
// the given kind ("ul" or "ol"), running each item's text through
|
|
||||||
// parseInline so bold/links etc. work inside list items.
|
|
||||||
func parseList(p *parser, kind, prefix string) *List {
|
func parseList(p *parser, kind, prefix string) *List {
|
||||||
var items []*ListItem
|
var items []*ListItem
|
||||||
for !p.done() {
|
for !p.done() {
|
||||||
|
|
@ -133,9 +128,7 @@ func parseList(p *parser, kind, prefix string) *List {
|
||||||
return &List{Kind: kind, Items: items}
|
return &List{Kind: kind, Items: items}
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseDefList groups consecutive "? term : definition" lines into a
|
// Groups consecutive "? term : definition" lines into a DefList node. Run content through parseInline.
|
||||||
// DefList. Each line is split on the first ":" into term and definition,
|
|
||||||
// both run through parseInline.
|
|
||||||
func parseDefList(p *parser) *DefList {
|
func parseDefList(p *parser) *DefList {
|
||||||
const prefix = "? "
|
const prefix = "? "
|
||||||
var items []*DefItem
|
var items []*DefItem
|
||||||
|
|
@ -154,8 +147,7 @@ func parseDefList(p *parser) *DefList {
|
||||||
return &DefList{Items: items}
|
return &DefList{Items: items}
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseKeyedTagLine builds a Quote, Link, Image, Video, or Audio node
|
// Builds a Quote, Link, Image, Video, or Audio node from a single "(keyword: ...)" line.
|
||||||
// from a single "(keyword: ...)" line.
|
|
||||||
func parseKeyedTagLine(line string) Node {
|
func parseKeyedTagLine(line string) Node {
|
||||||
trimmed := strings.TrimSpace(line)
|
trimmed := strings.TrimSpace(line)
|
||||||
inner := strings.TrimSuffix(strings.TrimPrefix(trimmed, "("), ")")
|
inner := strings.TrimSuffix(strings.TrimPrefix(trimmed, "("), ")")
|
||||||
|
|
@ -209,14 +201,14 @@ func parseKeyedTagLine(line string) Node {
|
||||||
return &Paragraph{Children: parseInline(line)}
|
return &Paragraph{Children: parseInline(line)}
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseAside builds an Aside from a single "{ ... }" line.
|
// Builds an Aside node from a single "{ ... }" line.
|
||||||
func parseAside(line string) *Aside {
|
func parseAside(line string) *Aside {
|
||||||
trimmed := strings.TrimSpace(line)
|
trimmed := strings.TrimSpace(line)
|
||||||
inner := strings.TrimSuffix(strings.TrimPrefix(trimmed, "{"), "}")
|
inner := strings.TrimSuffix(strings.TrimPrefix(trimmed, "{"), "}")
|
||||||
return &Aside{Children: parseInline(inner)}
|
return &Aside{Children: parseInline(inner)}
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseTodoList groups consecutive "[X] text" lines into one TodoList.
|
// Groups consecutive "[X] text" lines into one TodoList node.
|
||||||
func parseTodoList(p *parser) *TodoList {
|
func parseTodoList(p *parser) *TodoList {
|
||||||
var items []*TodoItem
|
var items []*TodoItem
|
||||||
for !p.done() {
|
for !p.done() {
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ func (document *Document) Render() template.HTML {
|
||||||
return template.HTML(b.String())
|
return template.HTML(b.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loops over nodes of the document's children to detect node types
|
// Loops over nodes of the document's children to detect node types and render them in HTML
|
||||||
func render(b *strings.Builder, nodes []Node) {
|
func render(b *strings.Builder, nodes []Node) {
|
||||||
for _, n := range nodes {
|
for _, n := range nodes {
|
||||||
switch v := n.(type) {
|
switch v := n.(type) {
|
||||||
|
|
@ -111,8 +111,8 @@ func render(b *strings.Builder, nodes []Node) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
b.WriteString("</figcaption>")
|
b.WriteString("</figcaption>")
|
||||||
b.WriteString("</figure>")
|
|
||||||
}
|
}
|
||||||
|
b.WriteString("</figure>")
|
||||||
case *Link:
|
case *Link:
|
||||||
b.WriteString(`<a href="`)
|
b.WriteString(`<a href="`)
|
||||||
b.WriteString(html.EscapeString(safeUrl(v.URL)))
|
b.WriteString(html.EscapeString(safeUrl(v.URL)))
|
||||||
|
|
@ -141,8 +141,9 @@ func render(b *strings.Builder, nodes []Node) {
|
||||||
b.WriteString("<figcaption>")
|
b.WriteString("<figcaption>")
|
||||||
if len(v.Caption) > 0 {
|
if len(v.Caption) > 0 {
|
||||||
render(b, v.Caption)
|
render(b, v.Caption)
|
||||||
|
b.WriteString(` | `)
|
||||||
}
|
}
|
||||||
b.WriteString(` | <small><a href="/uploaded/`)
|
b.WriteString(`<small><a href="/uploaded/`)
|
||||||
b.WriteString(html.EscapeString(safeUrl(v.URL)))
|
b.WriteString(html.EscapeString(safeUrl(v.URL)))
|
||||||
b.WriteString(`">Full size</a></small>`)
|
b.WriteString(`">Full size</a></small>`)
|
||||||
|
|
||||||
|
|
@ -221,6 +222,7 @@ func wrap(b *strings.Builder, tag string, children []Node) {
|
||||||
b.WriteString(">")
|
b.WriteString(">")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sanitizes urls for href and src attributes
|
||||||
func safeUrl(url string) string {
|
func safeUrl(url string) string {
|
||||||
trimmed := strings.TrimSpace(url)
|
trimmed := strings.TrimSpace(url)
|
||||||
|
|
||||||
|
|
@ -237,8 +239,7 @@ func safeUrl(url string) string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// plainText flattens inline nodes down to their visible text, ignoring
|
// Flattens inline nodes down to their visible text, ignoring markup like Bold/Em/Strike/Code. Used to create IDs or Aria Label text
|
||||||
// markup like Bold/Em/Strike/Code — just the words themselves.
|
|
||||||
func plainText(nodes []Node) string {
|
func plainText(nodes []Node) string {
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
for _, n := range nodes {
|
for _, n := range nodes {
|
||||||
|
|
@ -260,8 +261,7 @@ func plainText(nodes []Node) string {
|
||||||
return b.String()
|
return b.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// toKebabCase converts text into a lowercase, hyphen-separated slug
|
// Converts text into a lowercase hyphen-separated slug
|
||||||
// suitable for an HTML id (e.g. "Hello, World!" -> "hello-world").
|
|
||||||
func toKebabCase(text string) string {
|
func toKebabCase(text string) string {
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
lastWasHyphen := true // avoid a leading hyphen
|
lastWasHyphen := true // avoid a leading hyphen
|
||||||
|
|
@ -281,10 +281,8 @@ func toKebabCase(text string) string {
|
||||||
return strings.TrimRight(b.String(), "-")
|
return strings.TrimRight(b.String(), "-")
|
||||||
}
|
}
|
||||||
|
|
||||||
// imageURL prefixes local/relative image paths with "/processed/" so
|
// Allows prefixing local/relative image paths and normalizes ".jpeg" extension to ".jpg"
|
||||||
// they're served from the processed-images folder, and normalizes a
|
// Disabled in this version
|
||||||
// local ".jpeg" extension to ".jpg". Full http(s) URLs are left
|
|
||||||
// untouched, since they already point somewhere external.
|
|
||||||
func imageURL(url string) string {
|
func imageURL(url string) string {
|
||||||
if strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://") {
|
if strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://") {
|
||||||
return url
|
return url
|
||||||
|
|
|
||||||
|
|
@ -1,216 +0,0 @@
|
||||||
package kaku
|
|
||||||
|
|
||||||
import "testing"
|
|
||||||
|
|
||||||
func TestRender(t *testing.T) {
|
|
||||||
tests := map[string]struct {
|
|
||||||
doc *Document
|
|
||||||
want string
|
|
||||||
}{
|
|
||||||
"heading with anchor": {
|
|
||||||
&Document{Children: []Node{
|
|
||||||
&Heading{Level: 2, Children: []Node{&Text{Value: "Hello World!"}}},
|
|
||||||
}},
|
|
||||||
`<h2 id="hello-world">Hello World!<a href="#hello-world" aria-label="Hello World! permalink" class="heading-anchor" title="Link to this heading">#</a></h2>`,
|
|
||||||
},
|
|
||||||
"paragraph + bold + em + strike + code": {
|
|
||||||
&Document{Children: []Node{
|
|
||||||
&Paragraph{Children: []Node{
|
|
||||||
&Bold{Children: []Node{&Text{Value: "bold"}}},
|
|
||||||
&Text{Value: " and "},
|
|
||||||
&Em{Children: []Node{&Text{Value: "emphasis"}}},
|
|
||||||
&Text{Value: " and "},
|
|
||||||
&Strike{Children: []Node{&Text{Value: "strike"}}},
|
|
||||||
&Text{Value: " and "},
|
|
||||||
&Code{Value: "<style>"},
|
|
||||||
}},
|
|
||||||
}},
|
|
||||||
"<p><strong>bold</strong> and <em>emphasis</em> and <del>strike</del> and <code><style></code></p>",
|
|
||||||
},
|
|
||||||
"hr": {
|
|
||||||
&Document{Children: []Node{&HR{}}},
|
|
||||||
"<hr>",
|
|
||||||
},
|
|
||||||
"escaping": {
|
|
||||||
&Document{Children: []Node{
|
|
||||||
&Paragraph{Children: []Node{&Text{Value: "<script>"}}},
|
|
||||||
}},
|
|
||||||
"<p><script></p>",
|
|
||||||
},
|
|
||||||
"codeBlock": {
|
|
||||||
&Document{Children: []Node{
|
|
||||||
&CodeBlock{Content: "<script>const hello = Hello</script>"},
|
|
||||||
}},
|
|
||||||
"<pre><code><script>const hello = Hello</script></code></pre>",
|
|
||||||
},
|
|
||||||
"bullet list": {
|
|
||||||
&Document{Children: []Node{
|
|
||||||
&List{Kind: "ul", Items: []*ListItem{
|
|
||||||
{Children: []Node{&Text{Value: "AAA"}}},
|
|
||||||
{Children: []Node{&Text{Value: "BBB"}}},
|
|
||||||
{Children: []Node{&Text{Value: "CCC"}}},
|
|
||||||
}},
|
|
||||||
}},
|
|
||||||
"<ul><li>AAA</li><li>BBB</li><li>CCC</li></ul>",
|
|
||||||
},
|
|
||||||
"list with bold": {
|
|
||||||
&Document{Children: []Node{
|
|
||||||
&List{Kind: "ul", Items: []*ListItem{
|
|
||||||
{Children: []Node{
|
|
||||||
&Bold{Children: []Node{&Text{Value: "AAA"}}},
|
|
||||||
}},
|
|
||||||
{Children: []Node{&Text{Value: "BBB"}}},
|
|
||||||
}},
|
|
||||||
}},
|
|
||||||
"<ul><li><strong>AAA</strong></li><li>BBB</li></ul>",
|
|
||||||
},
|
|
||||||
"ordered list": {
|
|
||||||
&Document{Children: []Node{
|
|
||||||
&List{Kind: "ol", Items: []*ListItem{
|
|
||||||
{Children: []Node{&Text{Value: "Number 1"}}},
|
|
||||||
{Children: []Node{&Text{Value: "Number 2"}}},
|
|
||||||
{Children: []Node{&Text{Value: "Number 3"}}},
|
|
||||||
}},
|
|
||||||
}},
|
|
||||||
"<ol><li>Number 1</li><li>Number 2</li><li>Number 3</li></ol>",
|
|
||||||
},
|
|
||||||
"definition list": {
|
|
||||||
&Document{Children: []Node{
|
|
||||||
&DefList{Items: []*DefItem{
|
|
||||||
{
|
|
||||||
Term: []Node{&Text{Value: "Term 1"}},
|
|
||||||
Definition: []Node{&Text{Value: "definition 1"}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Term: []Node{&Text{Value: "Term 2"}},
|
|
||||||
Definition: []Node{&Text{Value: "definition 2"}},
|
|
||||||
},
|
|
||||||
}},
|
|
||||||
}},
|
|
||||||
"<dl><dt>Term 1</dt><dd>definition 1</dd><dt>Term 2</dt><dd>definition 2</dd></dl>",
|
|
||||||
},
|
|
||||||
"quote full": {
|
|
||||||
&Document{Children: []Node{
|
|
||||||
&Quote{
|
|
||||||
Text: []Node{&Text{Value: "And even, quotes!"}},
|
|
||||||
Author: "Author",
|
|
||||||
Source: "Source of quote",
|
|
||||||
Link: "url_of_quote",
|
|
||||||
},
|
|
||||||
}},
|
|
||||||
`<figure><blockquote cite="url_of_quote">And even, quotes!</blockquote><figcaption>—Author, <a href="url_of_quote">Source of quote</a></figcaption></figure>`,
|
|
||||||
},
|
|
||||||
"link full": {
|
|
||||||
&Document{Children: []Node{
|
|
||||||
&Link{
|
|
||||||
URL: "https://git.merveilles.town/thomasorus/Kaku",
|
|
||||||
Text: []Node{&Text{Value: "Check this link to the repo!"}},
|
|
||||||
Label: "Link to Kaku's repo",
|
|
||||||
Title: "Hover title",
|
|
||||||
},
|
|
||||||
}},
|
|
||||||
`<a href="https://git.merveilles.town/thomasorus/Kaku" aria-label="Link to Kaku's repo" title="Hover title">Check this link to the repo!</a>`,
|
|
||||||
},
|
|
||||||
"link minimal": {
|
|
||||||
&Document{Children: []Node{
|
|
||||||
&Link{URL: "https://example.com", Text: []Node{&Text{Value: "Example"}}},
|
|
||||||
}},
|
|
||||||
`<a href="https://example.com">Example</a>`,
|
|
||||||
},
|
|
||||||
"image with alt": {
|
|
||||||
&Document{Children: []Node{
|
|
||||||
&Image{URL: "img_url", Alt: "the alternate text"},
|
|
||||||
}},
|
|
||||||
`<img src="img_url" alt="the alternate text">`,
|
|
||||||
},
|
|
||||||
"image with caption": {
|
|
||||||
&Document{Children: []Node{
|
|
||||||
&Image{
|
|
||||||
URL: "img_url",
|
|
||||||
Alt: "the alternate text",
|
|
||||||
Caption: []Node{&Text{Value: "the text under the image"}},
|
|
||||||
},
|
|
||||||
}},
|
|
||||||
`<figure><img src="img_url" alt="the alternate text"><figcaption>the text under the image</figcaption></figure>`,
|
|
||||||
},
|
|
||||||
"paragraph with inline link": {
|
|
||||||
&Document{Children: []Node{
|
|
||||||
&Paragraph{Children: []Node{
|
|
||||||
&Text{Value: "Check out "},
|
|
||||||
&Link{URL: "https://example.com", Text: []Node{&Text{Value: "Example"}}},
|
|
||||||
&Text{Value: " for more."},
|
|
||||||
}},
|
|
||||||
}},
|
|
||||||
`<p>Check out <a href="https://example.com">Example</a> for more.</p>`,
|
|
||||||
},
|
|
||||||
"paragraph with inline image": {
|
|
||||||
&Document{Children: []Node{
|
|
||||||
&Paragraph{Children: []Node{
|
|
||||||
&Text{Value: "Look: "},
|
|
||||||
&Image{URL: "img_url", Alt: "alt text"},
|
|
||||||
}},
|
|
||||||
}},
|
|
||||||
`<p>Look: <img src="img_url" alt="alt text"></p>`,
|
|
||||||
},
|
|
||||||
"video plain": {
|
|
||||||
&Document{Children: []Node{&Video{URL: "videoUrl"}}},
|
|
||||||
`<video controls="" preload="metadata" src="videoUrl" type="video/mp4"></video>`,
|
|
||||||
},
|
|
||||||
"video autoplay/gif": {
|
|
||||||
&Document{Children: []Node{&Video{URL: "videoUrl", Autoplay: true}}},
|
|
||||||
`<video autoplay="true" playsinline="true" loop="true" muted="true" preload="metadata" src="videoUrl" type="video/mp4"></video>`,
|
|
||||||
},
|
|
||||||
"video with autoplay and figcaption": {
|
|
||||||
&Document{Children: []Node{
|
|
||||||
&Video{
|
|
||||||
URL: "aesthetics010.mp4",
|
|
||||||
Caption: []Node{&Text{Value: "Art by "}, &Link{URL: "https://twitter.com/lvl374", Text: []Node{&Text{Value: "lvl374"}}}},
|
|
||||||
Autoplay: true,
|
|
||||||
},
|
|
||||||
}},
|
|
||||||
`<figure><video autoplay="true" playsinline="true" loop="true" muted="true" preload="metadata" src="aesthetics010.mp4" type="video/mp4"></video><figcaption>Art by <a href="https://twitter.com/lvl374">lvl374</a></figcaption></figure>`,
|
|
||||||
},
|
|
||||||
"audio": {
|
|
||||||
&Document{Children: []Node{&Audio{URL: "audioUrl"}}},
|
|
||||||
`<audio controls="" preload="metadata" src="audioUrl" type="audio/mpeg"></audio>`,
|
|
||||||
},
|
|
||||||
"aside": {
|
|
||||||
&Document{Children: []Node{
|
|
||||||
&Aside{Children: []Node{&Text{Value: "A side note"}}},
|
|
||||||
}},
|
|
||||||
"<aside><p>Side note</p>A side note</aside>",
|
|
||||||
},
|
|
||||||
"details": {
|
|
||||||
&Document{Children: []Node{
|
|
||||||
&Details{
|
|
||||||
Summary: []Node{&Text{Value: "Click to expand"}},
|
|
||||||
Content: []Node{&Text{Value: "Hidden content here"}},
|
|
||||||
},
|
|
||||||
}},
|
|
||||||
"<details><summary>Click to expand</summary>Hidden content here</details>",
|
|
||||||
},
|
|
||||||
"todo list": {
|
|
||||||
&Document{Children: []Node{
|
|
||||||
&TodoList{Items: []*TodoItem{
|
|
||||||
{State: ' ', Children: []Node{&Text{Value: "Not started"}}},
|
|
||||||
{State: '@', Children: []Node{&Text{Value: "In progress"}}},
|
|
||||||
{State: '~', Children: []Node{&Text{Value: "Paused"}}},
|
|
||||||
{State: 'x', Children: []Node{&Text{Value: "Done"}}},
|
|
||||||
}},
|
|
||||||
}},
|
|
||||||
`<ul><li><code aria-hidden="true">[ ]</code><span class="visually-hidden">Opened task</span> Not started</li>` +
|
|
||||||
`<li><code aria-hidden="true">[@]</code><span class="visually-hidden">Ongoing task</span> In progress</li>` +
|
|
||||||
`<li><code aria-hidden="true">[~]</code><span class="visually-hidden">Paused or abandonned task</span> Paused</li>` +
|
|
||||||
`<li><code aria-hidden="true">[x]</code><span class="visually-hidden">Completed task</span> Done</li></ul>`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, c := range tests {
|
|
||||||
t.Run(name, func(t *testing.T) {
|
|
||||||
got := c.doc.Render()
|
|
||||||
if string(got) != c.want {
|
|
||||||
t.Errorf("got:\n %s\nwant:\n %s", got, c.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in a new issue