Refactor keyword parsing/rendering & add support for ATTR_HTML
This commit is contained in:
parent
a859264420
commit
81f74f4ad9
9 changed files with 169 additions and 76 deletions
|
@ -83,7 +83,11 @@ table, .highlight > pre, pre.example {
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
border: 1px solid rgba(250, 100, 50, 0.5); }
|
border: 1px solid rgba(250, 100, 50, 0.5); }
|
||||||
|
|
||||||
.caption {
|
figure {
|
||||||
|
margin: 1em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
figcaption {
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
font-size: 0.75em;
|
font-size: 0.75em;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
66
org/html.go
66
org/html.go
|
@ -4,6 +4,9 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"html"
|
"html"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
h "golang.org/x/net/html"
|
||||||
|
"golang.org/x/net/html/atom"
|
||||||
)
|
)
|
||||||
|
|
||||||
type HTMLWriter struct {
|
type HTMLWriter struct {
|
||||||
|
@ -45,10 +48,14 @@ func (w *HTMLWriter) emptyClone() *HTMLWriter {
|
||||||
return &wcopy
|
return &wcopy
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *HTMLWriter) before(d *Document) {
|
func (w *HTMLWriter) nodesAsString(nodes ...Node) string {
|
||||||
w.document = d
|
tmp := w.emptyClone()
|
||||||
|
tmp.writeNodes(nodes...)
|
||||||
|
return tmp.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (w *HTMLWriter) before(d *Document) {}
|
||||||
|
|
||||||
func (w *HTMLWriter) after(d *Document) {
|
func (w *HTMLWriter) after(d *Document) {
|
||||||
w.writeFootnotes(d)
|
w.writeFootnotes(d)
|
||||||
}
|
}
|
||||||
|
@ -199,9 +206,7 @@ func (w *HTMLWriter) writeRegularLink(l RegularLink) {
|
||||||
}
|
}
|
||||||
description := url
|
description := url
|
||||||
if l.Description != nil {
|
if l.Description != nil {
|
||||||
descriptionWriter := w.emptyClone()
|
description = w.nodesAsString(l.Description...)
|
||||||
descriptionWriter.writeNodes(l.Description...)
|
|
||||||
description = descriptionWriter.String()
|
|
||||||
}
|
}
|
||||||
switch l.Kind() {
|
switch l.Kind() {
|
||||||
case "image":
|
case "image":
|
||||||
|
@ -245,17 +250,27 @@ func (w *HTMLWriter) writeHorizontalRule(h HorizontalRule) {
|
||||||
w.WriteString("<hr>\n")
|
w.WriteString("<hr>\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *HTMLWriter) writeNodeWithMeta(m NodeWithMeta) {
|
func (w *HTMLWriter) writeNodeWithMeta(n NodeWithMeta) {
|
||||||
nodeW := w.emptyClone()
|
out := w.nodesAsString(n.Node)
|
||||||
nodeW.writeNodes(m.Node)
|
if p, ok := n.Node.(Paragraph); ok {
|
||||||
nodeString := nodeW.String()
|
if len(p.Children) == 1 && isImageOrVideoLink(p.Children[0]) {
|
||||||
if rawCaption, ok := m.Meta["CAPTION"]; ok {
|
out = w.nodesAsString(p.Children[0])
|
||||||
nodes, captionW := w.document.parseInline(rawCaption), w.emptyClone()
|
}
|
||||||
captionW.writeNodes(nodes...)
|
|
||||||
caption := `<p class="caption">` + "\n" + captionW.String() + "\n</p>\n"
|
|
||||||
nodeString = `<div class="captioned">` + "\n" + nodeString + caption + `</div>` + "\n"
|
|
||||||
}
|
}
|
||||||
w.WriteString(nodeString)
|
for _, attributes := range n.Meta.HTMLAttributes {
|
||||||
|
out = withHTMLAttributes(out, attributes...) + "\n"
|
||||||
|
}
|
||||||
|
if len(n.Meta.Caption) != 0 {
|
||||||
|
caption := ""
|
||||||
|
for i, ns := range n.Meta.Caption {
|
||||||
|
if i != 0 {
|
||||||
|
caption += " "
|
||||||
|
}
|
||||||
|
caption += w.nodesAsString(ns...)
|
||||||
|
}
|
||||||
|
out = fmt.Sprintf("<figure>\n%s<figcaption>\n%s\n</figcaption>\n</figure>\n", out, caption)
|
||||||
|
}
|
||||||
|
w.WriteString(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *HTMLWriter) writeTable(t Table) {
|
func (w *HTMLWriter) writeTable(t Table) {
|
||||||
|
@ -289,3 +304,24 @@ func (w *HTMLWriter) writeTableHeader(t TableHeader) {
|
||||||
func (w *HTMLWriter) writeTableSeparator(t TableSeparator) {
|
func (w *HTMLWriter) writeTableSeparator(t TableSeparator) {
|
||||||
w.WriteString("<tr></tr>\n")
|
w.WriteString("<tr></tr>\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func withHTMLAttributes(input string, kvs ...string) string {
|
||||||
|
if len(kvs)%2 != 0 {
|
||||||
|
panic(fmt.Sprintf("len of kvs must be even: %#v", kvs))
|
||||||
|
}
|
||||||
|
context := &h.Node{Type: h.ElementNode, Data: "body", DataAtom: atom.Body}
|
||||||
|
nodes, err := h.ParseFragment(strings.NewReader(strings.TrimSpace(input)), context)
|
||||||
|
if err != nil || len(nodes) != 1 {
|
||||||
|
panic(fmt.Sprintf("could not extend html attributes of %s: %v (%s)", input, len(nodes), err))
|
||||||
|
}
|
||||||
|
out, node := strings.Builder{}, nodes[0]
|
||||||
|
for i := 0; i < len(kvs)-1; i += 2 {
|
||||||
|
k, v := strings.TrimPrefix(kvs[i], ":"), kvs[i+1]
|
||||||
|
node.Attr = append(node.Attr, h.Attribute{Namespace: "", Key: k, Val: v})
|
||||||
|
}
|
||||||
|
err = h.Render(&out, nodes[0])
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("could not extend html attributes of %s: %#v (%s)", input, nodes, err))
|
||||||
|
}
|
||||||
|
return out.String()
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package org
|
package org
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/csv"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
@ -12,14 +13,18 @@ type Keyword struct {
|
||||||
|
|
||||||
type NodeWithMeta struct {
|
type NodeWithMeta struct {
|
||||||
Node Node
|
Node Node
|
||||||
Meta map[string]string
|
Meta Metadata
|
||||||
|
}
|
||||||
|
|
||||||
|
type Metadata struct {
|
||||||
|
Caption [][]Node
|
||||||
|
HTMLAttributes [][]string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Comment struct{ Content string }
|
type Comment struct{ Content string }
|
||||||
|
|
||||||
var keywordRegexp = regexp.MustCompile(`^(\s*)#\+([^:]+):(\s+(.*)|(\s*)$)`)
|
var keywordRegexp = regexp.MustCompile(`^(\s*)#\+([^:]+):(\s+(.*)|(\s*)$)`)
|
||||||
var commentRegexp = regexp.MustCompile(`^(\s*)#(.*)`)
|
var commentRegexp = regexp.MustCompile(`^(\s*)#(.*)`)
|
||||||
var affiliatedKeywordRegexp = regexp.MustCompile(`^(CAPTION)$`)
|
|
||||||
|
|
||||||
func lexKeywordOrComment(line string) (token, bool) {
|
func lexKeywordOrComment(line string) (token, bool) {
|
||||||
if m := keywordRegexp.FindStringSubmatch(line); m != nil {
|
if m := keywordRegexp.FindStringSubmatch(line); m != nil {
|
||||||
|
@ -30,47 +35,44 @@ func lexKeywordOrComment(line string) (token, bool) {
|
||||||
return nilToken, false
|
return nilToken, false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Document) parseKeyword(i int, stop stopFn) (int, Node) {
|
|
||||||
k := parseKeyword(d.tokens[i])
|
|
||||||
if affiliatedKeywordRegexp.MatchString(k.Key) {
|
|
||||||
consumed, node := d.parseAffiliated(i, stop)
|
|
||||||
if consumed != 0 {
|
|
||||||
return consumed, node
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
d.BufferSettings[k.Key] = strings.Join([]string{d.BufferSettings[k.Key], k.Value}, "\n")
|
|
||||||
}
|
|
||||||
return 1, k
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Document) parseComment(i int, stop stopFn) (int, Node) {
|
func (d *Document) parseComment(i int, stop stopFn) (int, Node) {
|
||||||
return 1, Comment{d.tokens[i].content}
|
return 1, Comment{d.tokens[i].content}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Document) parseAffiliated(i int, stop stopFn) (int, Node) {
|
func (d *Document) parseKeyword(i int, stop stopFn) (int, Node) {
|
||||||
start, meta := i, map[string]string{}
|
k := parseKeyword(d.tokens[i])
|
||||||
for ; !stop(d, i) && d.tokens[i].kind == "keyword"; i++ {
|
if k.Key == "CAPTION" || k.Key == "ATTR_HTML" {
|
||||||
k := parseKeyword(d.tokens[i])
|
consumed, node := d.parseAffiliated(i, stop)
|
||||||
if !affiliatedKeywordRegexp.MatchString(k.Key) {
|
if consumed != 0 {
|
||||||
return 0, nil
|
return consumed, node
|
||||||
}
|
}
|
||||||
if value, ok := meta[k.Key]; ok {
|
}
|
||||||
meta[k.Key] = value + " " + k.Value
|
d.BufferSettings[k.Key] = strings.Join([]string{d.BufferSettings[k.Key], k.Value}, "\n")
|
||||||
} else {
|
return 1, k
|
||||||
meta[k.Key] = k.Value
|
}
|
||||||
|
|
||||||
|
func (d *Document) parseAffiliated(i int, stop stopFn) (int, Node) {
|
||||||
|
start, meta := i, Metadata{}
|
||||||
|
for ; !stop(d, i) && d.tokens[i].kind == "keyword"; i++ {
|
||||||
|
switch k := parseKeyword(d.tokens[i]); k.Key {
|
||||||
|
case "CAPTION":
|
||||||
|
meta.Caption = append(meta.Caption, d.parseInline(k.Value))
|
||||||
|
case "ATTR_HTML":
|
||||||
|
r := csv.NewReader(strings.NewReader(k.Value))
|
||||||
|
r.Comma = ' '
|
||||||
|
attributes, err := r.Read()
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
meta.HTMLAttributes = append(meta.HTMLAttributes, attributes)
|
||||||
|
default:
|
||||||
|
return 0, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if stop(d, i) {
|
if stop(d, i) {
|
||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
consumed, node := 0, (Node)(nil)
|
consumed, node := d.parseOne(i, stop)
|
||||||
if t := d.tokens[i]; t.kind == "text" {
|
|
||||||
if nodes := d.parseInline(t.content); len(nodes) == 1 && isImageOrVideoLink(nodes[0]) {
|
|
||||||
consumed, node = 1, Paragraph{nodes[:1]}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
consumed, node = d.parseOne(i, stop)
|
|
||||||
}
|
|
||||||
if consumed == 0 || node == nil {
|
if consumed == 0 || node == nil {
|
||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
|
22
org/org.go
22
org/org.go
|
@ -173,11 +173,25 @@ func (w *OrgWriter) writeKeyword(k Keyword) {
|
||||||
w.WriteString(w.indent + fmt.Sprintf("#+%s: %s\n", k.Key, k.Value))
|
w.WriteString(w.indent + fmt.Sprintf("#+%s: %s\n", k.Key, k.Value))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *OrgWriter) writeNodeWithMeta(m NodeWithMeta) {
|
func (w *OrgWriter) writeNodeWithMeta(n NodeWithMeta) {
|
||||||
for k, v := range m.Meta {
|
for _, ns := range n.Meta.Caption {
|
||||||
w.writeNodes(Keyword{k, v})
|
w.WriteString("#+CAPTION: ")
|
||||||
|
w.writeNodes(ns...)
|
||||||
|
w.WriteString("\n")
|
||||||
}
|
}
|
||||||
w.writeNodes(m.Node)
|
for _, attributes := range n.Meta.HTMLAttributes {
|
||||||
|
w.WriteString("#+ATTR_HTML: ")
|
||||||
|
for i := 0; i < len(attributes)-1; i += 2 {
|
||||||
|
w.WriteString(attributes[i] + " ")
|
||||||
|
if strings.ContainsAny(attributes[i+1], "\t ") {
|
||||||
|
w.WriteString(`"` + attributes[i+1] + `"`)
|
||||||
|
} else {
|
||||||
|
w.WriteString(attributes[i+1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.WriteString("\n")
|
||||||
|
}
|
||||||
|
w.writeNodes(n.Node)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *OrgWriter) writeComment(c Comment) {
|
func (w *OrgWriter) writeComment(c Comment) {
|
||||||
|
|
8
org/testdata/blocks.html
vendored
8
org/testdata/blocks.html
vendored
|
@ -1,4 +1,4 @@
|
||||||
<div class="captioned">
|
<figure>
|
||||||
<div class="highlight">
|
<div class="highlight">
|
||||||
<pre>
|
<pre>
|
||||||
echo "a bash source block"
|
echo "a bash source block"
|
||||||
|
@ -10,10 +10,10 @@ function hello {
|
||||||
hello
|
hello
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
<p class="caption">
|
<figcaption>
|
||||||
block caption
|
block caption
|
||||||
</p>
|
</figcaption>
|
||||||
</div>
|
</figure>
|
||||||
<div class="highlight">
|
<div class="highlight">
|
||||||
<pre>
|
<pre>
|
||||||
a source block without a language
|
a source block without a language
|
||||||
|
|
31
org/testdata/captions.html
vendored
31
org/testdata/captions.html
vendored
|
@ -1,27 +1,30 @@
|
||||||
<p>
|
<p>
|
||||||
Anything can be captioned. Also captions are not real, correct captions but just a paragraph below the element (bothe wrapped into a div)
|
Anything can be captioned. Also captions are not real, correct captions but just a paragraph below the element (bothe wrapped into a div)
|
||||||
</p>
|
</p>
|
||||||
<div class="captioned">
|
<figure>
|
||||||
<div class="highlight">
|
<div class="highlight">
|
||||||
<pre>
|
<pre>
|
||||||
echo "i have a caption!"
|
echo "i have a caption!"
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
<p class="caption">
|
<figcaption>
|
||||||
captioned soure block
|
captioned soure block
|
||||||
</p>
|
</figcaption>
|
||||||
</div>
|
</figure>
|
||||||
<div class="captioned">
|
<figure>
|
||||||
|
<img src="http://placekitten.com/200/200#.png" alt="http://placekitten.com/200/200#.png" title="http://placekitten.com/200/200#.png" /><figcaption>
|
||||||
|
captioned link (image in this case)
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
<p>
|
<p>
|
||||||
<video src="my-video.mp4" title="my-video.mp4">my-video.mp4</video>
|
note that the whole paragraph is captioned, so a linebreak is needed for images to caption correctly
|
||||||
</p>
|
</p>
|
||||||
<p class="caption">
|
<figure>
|
||||||
captioned link (video in this case)
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<p>
|
<p>
|
||||||
note that only that one line is captioned, not the whole paragraph
|
<img src="http://placekitten.com/200/200#.png" alt="http://placekitten.com/200/200#.png" title="http://placekitten.com/200/200#.png" />
|
||||||
</p>
|
see?
|
||||||
<p>
|
|
||||||
also, normal text lines can't be captioned
|
|
||||||
</p>
|
</p>
|
||||||
|
<figcaption>
|
||||||
|
captioned link (image in this case)
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
13
org/testdata/captions.org
vendored
13
org/testdata/captions.org
vendored
|
@ -5,9 +5,12 @@ Anything can be captioned. Also captions are not real, correct captions but just
|
||||||
echo "i have a caption!"
|
echo "i have a caption!"
|
||||||
#+END_SRC
|
#+END_SRC
|
||||||
|
|
||||||
#+CAPTION: captioned link (video in this case)
|
#+CAPTION: captioned link (image in this case)
|
||||||
[[my-video.mp4]]
|
[[http://placekitten.com/200/200#.png]]
|
||||||
note that only that one line is captioned, not the whole paragraph
|
|
||||||
|
note that the whole paragraph is captioned, so a linebreak is needed for images to caption correctly
|
||||||
|
|
||||||
|
#+CAPTION: captioned link (image in this case)
|
||||||
|
[[http://placekitten.com/200/200#.png]]
|
||||||
|
see?
|
||||||
|
|
||||||
#+CAPTION: not happening!
|
|
||||||
also, normal text lines can't be captioned
|
|
||||||
|
|
18
org/testdata/keywords.html
vendored
Normal file
18
org/testdata/keywords.html
vendored
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
<figure>
|
||||||
|
<div class="highlight" class="a b c" id="it">
|
||||||
|
<pre>echo "a bash source block with custom html attributes"
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
<figcaption>
|
||||||
|
and <span style="text-decoration: underline;">multiple</span> lines of <strong>captions</strong>!
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
<p>
|
||||||
|
and an image with custom html attributes and a caption
|
||||||
|
</p>
|
||||||
|
<figure>
|
||||||
|
<img src="http://placekitten.com/200/200#.png" alt="http://placekitten.com/200/200#.png" title="http://placekitten.com/200/200#.png" style="border: 10px solid black"/>
|
||||||
|
<figcaption>
|
||||||
|
kittens!
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
13
org/testdata/keywords.org
vendored
Normal file
13
org/testdata/keywords.org
vendored
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
|
||||||
|
#+CAPTION: and _multiple_
|
||||||
|
#+CAPTION: lines of *captions*!
|
||||||
|
#+ATTR_HTML: :class "a b c"
|
||||||
|
#+ATTR_HTML: :id it
|
||||||
|
#+BEGIN_SRC sh
|
||||||
|
echo "a bash source block with custom html attributes"
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
and an image with custom html attributes and a caption
|
||||||
|
#+CAPTION: kittens!
|
||||||
|
#+ATTR_HTML: :style "border: 10px solid black"
|
||||||
|
[[http://placekitten.com/200/200#.png]]
|
Loading…
Add table
Add a link
Reference in a new issue