diff --git a/etc/style.css b/etc/style.css index 163c26c..457a982 100644 --- a/etc/style.css +++ b/etc/style.css @@ -83,7 +83,11 @@ table, .highlight > pre, pre.example { font-size: 0.85rem; border: 1px solid rgba(250, 100, 50, 0.5); } -.caption { +figure { + margin: 1em 0; +} + +figcaption { font-family: monospace; font-size: 0.75em; text-align: center; diff --git a/org/html.go b/org/html.go index 66e5deb..a7b11b4 100644 --- a/org/html.go +++ b/org/html.go @@ -4,6 +4,9 @@ import ( "fmt" "html" "strings" + + h "golang.org/x/net/html" + "golang.org/x/net/html/atom" ) type HTMLWriter struct { @@ -45,10 +48,14 @@ func (w *HTMLWriter) emptyClone() *HTMLWriter { return &wcopy } -func (w *HTMLWriter) before(d *Document) { - w.document = d +func (w *HTMLWriter) nodesAsString(nodes ...Node) string { + tmp := w.emptyClone() + tmp.writeNodes(nodes...) + return tmp.String() } +func (w *HTMLWriter) before(d *Document) {} + func (w *HTMLWriter) after(d *Document) { w.writeFootnotes(d) } @@ -199,9 +206,7 @@ func (w *HTMLWriter) writeRegularLink(l RegularLink) { } description := url if l.Description != nil { - descriptionWriter := w.emptyClone() - descriptionWriter.writeNodes(l.Description...) - description = descriptionWriter.String() + description = w.nodesAsString(l.Description...) } switch l.Kind() { case "image": @@ -245,17 +250,27 @@ func (w *HTMLWriter) writeHorizontalRule(h HorizontalRule) { w.WriteString("
\n") } -func (w *HTMLWriter) writeNodeWithMeta(m NodeWithMeta) { - nodeW := w.emptyClone() - nodeW.writeNodes(m.Node) - nodeString := nodeW.String() - if rawCaption, ok := m.Meta["CAPTION"]; ok { - nodes, captionW := w.document.parseInline(rawCaption), w.emptyClone() - captionW.writeNodes(nodes...) - caption := `

` + "\n" + captionW.String() + "\n

\n" - nodeString = `
` + "\n" + nodeString + caption + `
` + "\n" +func (w *HTMLWriter) writeNodeWithMeta(n NodeWithMeta) { + out := w.nodesAsString(n.Node) + if p, ok := n.Node.(Paragraph); ok { + if len(p.Children) == 1 && isImageOrVideoLink(p.Children[0]) { + out = w.nodesAsString(p.Children[0]) + } } - 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("
\n%s
\n%s\n
\n
\n", out, caption) + } + w.WriteString(out) } func (w *HTMLWriter) writeTable(t Table) { @@ -289,3 +304,24 @@ func (w *HTMLWriter) writeTableHeader(t TableHeader) { func (w *HTMLWriter) writeTableSeparator(t TableSeparator) { w.WriteString("\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() +} diff --git a/org/keyword.go b/org/keyword.go index cbb4829..d4d0399 100644 --- a/org/keyword.go +++ b/org/keyword.go @@ -1,6 +1,7 @@ package org import ( + "encoding/csv" "regexp" "strings" ) @@ -12,14 +13,18 @@ type Keyword struct { type NodeWithMeta struct { Node Node - Meta map[string]string + Meta Metadata +} + +type Metadata struct { + Caption [][]Node + HTMLAttributes [][]string } type Comment struct{ Content string } var keywordRegexp = regexp.MustCompile(`^(\s*)#\+([^:]+):(\s+(.*)|(\s*)$)`) var commentRegexp = regexp.MustCompile(`^(\s*)#(.*)`) -var affiliatedKeywordRegexp = regexp.MustCompile(`^(CAPTION)$`) func lexKeywordOrComment(line string) (token, bool) { if m := keywordRegexp.FindStringSubmatch(line); m != nil { @@ -30,47 +35,44 @@ func lexKeywordOrComment(line string) (token, bool) { 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) { return 1, Comment{d.tokens[i].content} } -func (d *Document) parseAffiliated(i int, stop stopFn) (int, Node) { - start, meta := i, map[string]string{} - for ; !stop(d, i) && d.tokens[i].kind == "keyword"; i++ { - k := parseKeyword(d.tokens[i]) - if !affiliatedKeywordRegexp.MatchString(k.Key) { - return 0, nil +func (d *Document) parseKeyword(i int, stop stopFn) (int, Node) { + k := parseKeyword(d.tokens[i]) + if k.Key == "CAPTION" || k.Key == "ATTR_HTML" { + consumed, node := d.parseAffiliated(i, stop) + if consumed != 0 { + return consumed, node } - if value, ok := meta[k.Key]; ok { - meta[k.Key] = value + " " + k.Value - } else { - meta[k.Key] = k.Value + } + d.BufferSettings[k.Key] = strings.Join([]string{d.BufferSettings[k.Key], k.Value}, "\n") + return 1, k +} + +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) { return 0, nil } - consumed, node := 0, (Node)(nil) - 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) - } + consumed, node := d.parseOne(i, stop) if consumed == 0 || node == nil { return 0, nil } diff --git a/org/org.go b/org/org.go index 87b6d58..3408a34 100644 --- a/org/org.go +++ b/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)) } -func (w *OrgWriter) writeNodeWithMeta(m NodeWithMeta) { - for k, v := range m.Meta { - w.writeNodes(Keyword{k, v}) +func (w *OrgWriter) writeNodeWithMeta(n NodeWithMeta) { + for _, ns := range n.Meta.Caption { + 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) { diff --git a/org/testdata/blocks.html b/org/testdata/blocks.html index 3e2e69d..1e26208 100644 --- a/org/testdata/blocks.html +++ b/org/testdata/blocks.html @@ -1,4 +1,4 @@ -
+
 echo "a bash source block"
@@ -10,10 +10,10 @@ function hello {
 hello
 
-

+

block caption -

-
+ +
 a source block without a language
diff --git a/org/testdata/captions.html b/org/testdata/captions.html
index 138f066..a964c43 100644
--- a/org/testdata/captions.html
+++ b/org/testdata/captions.html
@@ -1,27 +1,30 @@
 

Anything can be captioned. Also captions are not real, correct captions but just a paragraph below the element (bothe wrapped into a div)

-
+
 echo "i have a caption!"
 
-

+

captioned soure block -

-
-
+ + +
+http://placekitten.com/200/200#.png
+captioned link (image in this case) +
+

- +note that the whole paragraph is captioned, so a linebreak is needed for images to caption correctly

-

-captioned link (video in this case) -

-
+

-note that only that one line is captioned, not the whole paragraph -

-

-also, normal text lines can't be captioned +http://placekitten.com/200/200#.png +see?

+
+captioned link (image in this case) +
+
diff --git a/org/testdata/captions.org b/org/testdata/captions.org index 3822b29..87b6e40 100644 --- a/org/testdata/captions.org +++ b/org/testdata/captions.org @@ -5,9 +5,12 @@ Anything can be captioned. Also captions are not real, correct captions but just echo "i have a caption!" #+END_SRC -#+CAPTION: captioned link (video in this case) -[[my-video.mp4]] -note that only that one line is captioned, not the whole paragraph +#+CAPTION: captioned link (image in this case) +[[http://placekitten.com/200/200#.png]] + +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 diff --git a/org/testdata/keywords.html b/org/testdata/keywords.html new file mode 100644 index 0000000..a108f03 --- /dev/null +++ b/org/testdata/keywords.html @@ -0,0 +1,18 @@ +
+
+
echo "a bash source block with custom html attributes"
+
+
+
+and multiple lines of captions! +
+
+

+and an image with custom html attributes and a caption +

+
+http://placekitten.com/200/200#.png +
+kittens! +
+
diff --git a/org/testdata/keywords.org b/org/testdata/keywords.org new file mode 100644 index 0000000..cba15b3 --- /dev/null +++ b/org/testdata/keywords.org @@ -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]]