odt.c (58773B)
/* * Copyright (c) 2021 Kristaps Dzonsons <kristaps@bsd.lv> * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ #include "config.h" #if HAVE_SYS_QUEUE # include <sys/queue.h> #endif #include <assert.h> #include <ctype.h> #include <inttypes.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <time.h> #include "lowdown.h" #include "extern.h" /* * Maximum length of any style. This should account for fixed prefix * text (e.g., "Frame" at longest) then an incrementing size_t. */ #define STYLE_NAME_LEN 32 /* * Default size for a blockquote (paragraph indent). */ static const float TAB_LEN = 1.25; /* * Default size for a list indent. Lists are first indented by the * number of tabs (starting at zero), then giving a full list indent, * then each sub-list gets half again this. */ static const float LIST_LEN = 1.27; /* * A style in <office-automatic-styles>. */ struct odt_sty { char name[STYLE_NAME_LEN]; /* name */ size_t offs; /* offset ("tabs") from zero */ size_t parent; /* parent or (size_t)-1*/ enum lowdown_rndrt type; /* specific type of style */ int foot; /* in a footnote */ int fmt; /* general type of style */ #define ODT_STY_TEXT 1 /* text (inline) */ #define ODT_STY_PARA 2 /* paragraph */ #define ODT_STY_UL 3 /* unordered list */ #define ODT_STY_OL 4 /* ordered list */ #define ODT_STY_H1 5 /* h1 heading */ #define ODT_STY_H2 6 /* h2 heading */ #define ODT_STY_H3 7 /* h3 heading */ #define ODT_STY_TBL 8 /* table */ #define ODT_STY_TBL_PARA 9 /* table contents */ #define ODT_STY_LIT 10 /* literal */ }; /* * A change. I'm not sure we'll need anything but "ins", so this could * just be an array of int, but whatever. */ struct odt_chng { int ins; /* inserted vs deleted */ }; /* * Our internal state object. Beyond retaining our flags, this also * keeps output state in terms of the styles that need printing. */ struct odt { struct hentryq headers_used; /* headers we've seen */ ssize_t headers_offs; /* header offset */ unsigned int flags; /* "oflags" in lowdown_opts */ struct odt_sty *stys; /* styles for content */ size_t stysz; /* number of styles */ size_t sty_T; /* "T" styles */ size_t sty_Table; /* "Table" styles */ size_t sty_L; /* "L" styles */ size_t sty_P; /* "P" styles */ size_t offs; /* offs or (size_t)-1 in list */ size_t list; /* root list style or (size_t)-1 */ int foot; /* in footnote or not */ size_t footcount; /* footnote */ struct odt_chng *chngs; /* changes in content */ size_t chngsz; /* number of changes */ char *sty; /* external styles or NULL */ }; static int rndr(struct lowdown_buf *, struct lowdown_metaq *, void *, const struct lowdown_node *); /* * Append a new zeroed style with an unset parent. Return NULL on * memory failure or the new style. */ static struct odt_sty * odt_style_add(struct odt *st) { void *pp; pp = reallocarray(st->stys, st->stysz + 1, sizeof(struct odt_sty)); if (pp == NULL) return NULL; st->stys = pp; memset(&st->stys[st->stysz], 0, sizeof(struct odt_sty)); st->stys[st->stysz].parent = (size_t)-1; return &st->stys[st->stysz++]; } /* * Create or fetch an inline style corresponding to the node type. * Return NULL on error or the style name on success. */ static const char * odt_style_add_text(struct odt *st, enum lowdown_rndrt type) { size_t i; struct odt_sty *s; for (i = 0; i < st->stysz; i++) if (st->stys[i].type == type) { assert(st->stys[i].fmt == ODT_STY_TEXT); return st->stys[i].name; } if ((s = odt_style_add(st)) == NULL) return NULL; s->fmt = ODT_STY_TEXT; s->type = type; snprintf(s->name, sizeof(s->name), "T%zu", st->sty_T++); return s->name; } /* * Flush out all of the styles and automatic styles. Return FALSE on * failure, TRUE on success. */ static int odt_sty_flush(struct lowdown_buf *ob, const struct odt *st, const struct odt_sty *sty) { size_t i; /* * Lists and non-lists have a different XML element name, and * non-lists designate whether in-line or paragraphs. */ if (sty->type == LOWDOWN_LIST && !HBUF_PUTSL(ob, "<text:list-style")) return 0; if (sty->type != LOWDOWN_LIST && !HBUF_PUTSL(ob, "<style:style")) return 0; switch (sty->fmt) { case ODT_STY_TEXT: if (!HBUF_PUTSL(ob, " style:family=\"text\"")) return 0; break; case ODT_STY_TBL_PARA: case ODT_STY_PARA: case ODT_STY_LIT: case ODT_STY_H1: case ODT_STY_H2: case ODT_STY_H3: if (!HBUF_PUTSL(ob, " style:family=\"paragraph\"")) return 0; break; case ODT_STY_TBL: if (!HBUF_PUTSL(ob, " style:family=\"table\"")) return 0; break; } if (!hbuf_printf(ob, " style:name=\"%s\"", sty->name)) return 0; /* * Paragraphs in lists need to link to the list, then set some * other crap found in libreoffice output. */ switch (sty->fmt) { case ODT_STY_LIT: if (!HBUF_PUTSL(ob, " style:parent-style-name=\"Preformatted_20_Text\"")) return 0; break; case ODT_STY_PARA: if (!sty->foot && !HBUF_PUTSL(ob, " style:parent-style-name=\"Standard\"")) return 0; if (sty->foot && !HBUF_PUTSL(ob, " style:parent-style-name=\"Footnote\"")) return 0; if (sty->parent != (size_t)-1 && !hbuf_printf(ob, " style:list-style-name=\"%s\"", st->stys[sty->parent].name)) return 0; break; case ODT_STY_TBL_PARA: if (sty->foot && !HBUF_PUTSL(ob, " style:parent-style-name=\"Footnote\"")) return 0; if (!sty->foot && !HBUF_PUTSL(ob, " style:parent-style-name=\"Table_20_Contents\"")) return 0; break; case ODT_STY_H1: if (!HBUF_PUTSL(ob, " style:parent-style-name=\"Heading_20_1\"")) return 0; break; case ODT_STY_H2: if (!HBUF_PUTSL(ob, " style:parent-style-name=\"Heading_20_2\"")) return 0; break; case ODT_STY_H3: if (!HBUF_PUTSL(ob, " style:parent-style-name=\"Heading_20_3\"")) return 0; break; default: break; } if (!HBUF_PUTSL(ob, ">\n")) return 0; /* * I'm not sure what in this is necessary and what isn't yet. * The template followed is from libreoffice output. */ switch (sty->type) { case LOWDOWN_TABLE_BLOCK: if (!hbuf_printf(ob, "<style:table-properties" " fo:margin-left=\"%.3fcm\"" " fo:margin-right=\"0cm\"" " table:align=\"margins\"/>\n", sty->offs * TAB_LEN)) return 0; break; case LOWDOWN_HEADER: break; case LOWDOWN_PARAGRAPH: if (sty->offs == 0) break; if (!hbuf_printf(ob, "<style:paragraph-properties" " fo:margin-left=\"%.3fcm\"" " fo:margin-right=\"0cm\"" " fo:text-indent=\"0cm\"" " style:auto-text-indent=\"false\"/>\n", sty->offs * TAB_LEN)) return 0; break; case LOWDOWN_LIST: for (i = 0; i < 10; i++) { if (sty->fmt == ODT_STY_OL && !hbuf_printf(ob, "<text:list-level-style-number" " text:level=\"%zu\"" " text:style-name=\"Numbering_20_Symbols\"" " style:num-suffix=\".\"" " style:num-format=\"1\">\n" "<style:list-level-properties" " text:list-level-position-and-space-mode=" "\"label-alignment\">\n" "<style:list-level-label-alignment" " text:label-followed-by=\"listtab\"" " text:list-tab-stop-position=\"%.3fcm\"" " fo:text-indent=\"-0.635cm\"" " fo:margin-left=\"%.3fcm\"/>\n" "</style:list-level-properties>\n" "</text:list-level-style-number>\n", i + 1, (TAB_LEN * sty->offs) + LIST_LEN + ((LIST_LEN / 2.0) * i), (TAB_LEN * sty->offs) + LIST_LEN + ((LIST_LEN / 2.0) * i))) return 0; if (sty->fmt == ODT_STY_UL && !hbuf_printf(ob, "<text:list-level-style-bullet" " text:level=\"%zu\"" " text:style-name=\"Bullet_20_Symbols\"" " text:bullet-char=\"•\">\n" "<style:list-level-properties" " text:list-level-position-and-space-mode=" "\"label-alignment\">\n" "<style:list-level-label-alignment" " text:label-followed-by=\"listtab\"" " text:list-tab-stop-position=\"%.3fcm\"" " fo:text-indent=\"-0.635cm\"" " fo:margin-left=\"%.3fcm\"/>\n" "</style:list-level-properties>\n" "</text:list-level-style-bullet>\n", i + 1, (TAB_LEN * sty->offs) + LIST_LEN + ((LIST_LEN / 2.0) * i), (TAB_LEN * sty->offs) + LIST_LEN + ((LIST_LEN / 2.0) * i))) return 0; } break; case LOWDOWN_SUPERSCRIPT: if (!HBUF_PUTSL(ob, "<style:text-properties" " style:text-position=\"super 58%\"/>\n")) return 0; break; case LOWDOWN_TRIPLE_EMPHASIS: if (!HBUF_PUTSL(ob, "<style:text-properties" " fo:font-style=\"italic\"" " style:font-style-asian=\"italic\"" " style:font-style-complex=\"italic\"" " fo:font-weight=\"bold\"" " style:font-weight-asian=\"bold\"" " style:font-weight-complex=\"bold\"/>\n")) return 0; break; case LOWDOWN_DOUBLE_EMPHASIS: if (!HBUF_PUTSL(ob, "<style:text-properties" " fo:font-weight=\"bold\"" " style:font-weight-asian=\"bold\"" " style:font-weight-complex=\"bold\"/>\n")) return 0; break; case LOWDOWN_EMPHASIS: if (!HBUF_PUTSL(ob, "<style:text-properties" " fo:font-style=\"italic\"" " style:font-style-asian=\"italic\"" " style:font-style-complex=\"italic\"/>\n")) return 0; break; case LOWDOWN_STRIKETHROUGH: if (!HBUF_PUTSL(ob, "<style:text-properties" " style:text-line-through-style=\"solid\"" " style:text-line-through-type=\"single\"/>\n")) return 0; break; case LOWDOWN_HIGHLIGHT: if (!HBUF_PUTSL(ob, "<style:text-properties" " style:text-underline-style=\"solid\"" " style:text-underline-color=\"font-color\"" " style:text-underline-width=\"auto\"/>\n")) return 0; break; default: abort(); /* NOTREACHED */ } if (sty->type == LOWDOWN_LIST && !HBUF_PUTSL(ob, "</text:list-style>\n")) return 0; if (sty->type != LOWDOWN_LIST && !HBUF_PUTSL(ob, "</style:style>\n")) return 0; return 1; } /* * Flush out the "fixed" styles we need for standalone mode. * XXX: it's possible to put a lot of this into a separate file, * somehow, but that's a matter for the future. Return FALSE on * failure, TRUE on success. */ static int odt_styles_flush_fixed(struct lowdown_buf *ob, const struct odt *st) { if (st->sty != NULL) return hbuf_puts(ob, st->sty); if (!HBUF_PUTSL(ob, "<office:font-face-decls>\n" "<style:font-face style:name=\"OpenSymbol\"" " svg:font-family=\"OpenSymbol\"" " style:font-charset=\"x-symbol\"/>\n" "<style:font-face style:name=\"Liberation Mono\"" " svg:font-family=\"'Liberation Mono'\"" " style:font-family-generic=\"modern\"" " style:font-pitch=\"fixed\"/>\n" "<style:font-face style:name=\"Liberation Serif\"" " svg:font-family=\"'Liberation Serif'\"" " style:font-family-generic=\"roman\"" " style:font-pitch=\"variable\"/>\n" "<style:font-face style:name=\"Liberation Sans\"" " svg:font-family=\"'Liberation Sans'\"" " style:font-family-generic=\"swiss\"" " style:font-pitch=\"variable\"/>\n" "</office:font-face-decls>\n")) return 0; /* * This doesn't appear to make a difference if it's specified or * not, but I'm adding it because libreoffice does. */ if (!HBUF_PUTSL(ob, "<office:scripts>\n" " <office:script script:language=\"ooo:Basic\">\n" " <ooo:libraries xmlns:ooo=\"http://openoffice.org/2004/office\"" " xmlns:xlink=\"http://www.w3.org/1999/xlink\"/>\n" " </office:script>\n" "</office:scripts>\n")) return 0; if (!HBUF_PUTSL(ob, "<office:styles>\n")) return 0; /* Baseline. */ if (!HBUF_PUTSL(ob, "<style:style" " style:name=\"Standard\"" " style:family=\"paragraph\"" " style:class=\"text\"/>\n")) return 0; /* Text within block. */ if (!HBUF_PUTSL(ob, "<style:style" " style:name=\"Text_20_body\"" " style:display-name=\"Text body\"" " style:family=\"paragraph\"" " style:parent-style-name=\"Standard\"" " style:class=\"text\">\n" "<style:paragraph-properties" " fo:margin-top=\"0cm\"" " fo:margin-bottom=\"0.247cm\"" " style:contextual-spacing=\"false\"" " fo:line-height=\"115%\"/>\n" "</style:style>\n")) return 0; /* Horizontal line. */ if (!HBUF_PUTSL(ob, "<style:style" " style:family=\"paragraph\"" " style:name=\"Horizontal_20_Line\"" " style:parent-style-name=\"Standard\"" " style:display-name=\"Horizontal Line\"" " style:next-style-name=\"Text_20_body\"" " style:class=\"html\">\n" "<style:paragraph-properties" " fo:margin-top=\"0cm\"" " fo:margin-bottom=\"0.499cm\"" " style:contextual-spacing=\"false\"" " style:border-line-width-bottom=\"0.002cm 0.004cm 0.002cm\"" " fo:padding=\"0cm\"" " fo:border-left=\"none\"" " fo:border-right=\"none\"" " fo:border-top=\"none\"" " fo:border-bottom=\"0.14pt double #808080\"" " text:number-lines=\"false\"" " text:line-number=\"0\"" " style:join-border=\"false\"/>\n" "<style:text-properties" " fo:font-size=\"6pt\"" " style:font-size-asian=\"6pt\"" " style:font-size-complex=\"6pt\"/>\n" "</style:style>\n")) return 0; /* Images. */ if (!HBUF_PUTSL(ob, "<style:style style:name=\"Graphics\" style:family=\"graphic\">\n" "<style:graphic-properties" " text:anchor-type=\"paragraph\"" " svg:x=\"0cm\" svg:y=\"0cm\"" " style:wrap=\"dynamic\"" " style:number-wrapped-paragraphs=\"no-limit\"" " style:wrap-contour=\"false\"" " draw:auto-grow-height=\"true\"" " draw:auto-grow-width=\"true\"" " style:vertical-pos=\"top\"" " style:vertical-rel=\"paragraph\"" " style:horizontal-pos=\"center\"" " style:horizontal-rel=\"paragraph\"/>" "</style:style>")) return 0; /* Internet link. */ if (!HBUF_PUTSL(ob, "<style:style" " style:family=\"text\"" " style:name=\"Internet_20_Link\"" " style:display-name=\"Internet Link\">\n" "<style:text-properties" " fo:color=\"#000080\"" " loext:opacity=\"100%\"" " fo:language=\"zxx\"" " fo:country=\"none\"" " style:language-asian=\"zxx\"" " style:country-asian=\"none\"" " style:language-complex=\"zxx\"" " style:country-complex=\"none\"" " style:text-underline-style=\"solid\"" " style:text-underline-color=\"font-color\"" " style:text-underline-width=\"auto\"/>\n" "</style:style>\n")) return 0; /* Source (preformatted) code. */ if (!HBUF_PUTSL(ob, "<style:style" " style:family=\"text\"" " style:name=\"Source_20_Text\"" " style:display-name=\"Source Text\">\n" "<style:text-properties" " style:font-name=\"Liberation Mono\"" " fo:font-family=\"'Liberation Mono'\"" " style:font-family-generic=\"modern\"" " style:font-pitch=\"fixed\"" " style:font-name-asian=\"Liberation Mono\"" " style:font-family-asian=" "\"'Liberation Mono'\"" " style:font-family-generic-asian=\"modern\"" " style:font-pitch-asian=\"fixed\"" " style:font-name-complex=\"Liberation Mono\"" " style:font-family-complex=" "\"'Liberation Mono'\"" " style:font-family-generic-complex=\"modern\"" " style:font-pitch-complex=\"fixed\"/>\n" "</style:style>\n")) return 0; /* Frame (tables). */ if (!HBUF_PUTSL(ob, "<style:style" " style:name=\"Frame\"" " style:family=\"graphic\">\n" "<style:graphic-properties" " text:anchor-type=\"as-char\"" " svg:x=\"0cm\"" " svg:y=\"0cm\"" " fo:margin-left=\"0cm\"" " fo:margin-right=\"0cm\"" " fo:margin-top=\"0.201cm\"" " fo:margin-bottom=\"0.201cm\"" " style:wrap=\"parallel\"" " style:number-wrapped-paragraphs=\"no-limit\"" " style:wrap-contour=\"false\"" " style:vertical-pos=\"top\"" " style:vertical-rel=\"paragraph-content\"" " style:horizontal-pos=\"center\"" " style:horizontal-rel=\"paragraph-content\"" " fo:padding=\"0cm\"" " fo:border=\"0pt solid #000000\"/>\n" "</style:style>\n")) return 0; /* Preformatted text. */ if (!HBUF_PUTSL(ob, "<style:style" " style:name=\"Preformatted_20_Text\"" " style:display-name=\"Preformatted Text\"" " style:family=\"paragraph\"" " style:parent-style-name=\"Standard\"" " style:class=\"html\">\n" "<style:paragraph-properties" " fo:margin-top=\"0cm\"" " fo:margin-bottom=\"0cm\"" " style:contextual-spacing=\"false\"/>\n" "<style:text-properties" " style:font-name=\"Liberation Mono\"" " fo:font-family=\"'Liberation Mono'\"" " style:font-family-generic=\"modern\"" " style:font-pitch=\"fixed\"" " fo:font-size=\"10pt\"" " style:font-name-asian=\"Liberation Mono\"" " style:font-family-asian=\"'Liberation Mono'\"" " style:font-family-generic-asian=\"modern\"" " style:font-pitch-asian=\"fixed\"" " style:font-size-asian=\"10pt\"" " style:font-name-complex=\"Liberation Mono\"" " style:font-family-complex=\"'Liberation Mono'\"" " style:font-family-generic-complex=\"modern\"" " style:font-pitch-complex=\"fixed\"" " style:font-size-complex=\"10pt\"/>\n" "</style:style>\n")) return 0; /* Table contents. */ if (!HBUF_PUTSL(ob, "<style:style" " style:name=\"Table_20_Contents\"" " style:display-name=\"Table Contents\"" " style:family=\"paragraph\"" " style:parent-style-name=\"Standard\"" " style:class=\"extra\">\n" "<style:paragraph-properties" " fo:orphans=\"0\"" " fo:widows=\"0\"" " text:number-lines=\"false\"" " text:line-number=\"0\"/>\n" "</style:style>\n")) return 0; /* Headings. */ if (!HBUF_PUTSL(ob, "<style:style" " style:name=\"Heading\"" " style:family=\"paragraph\"" " style:parent-style-name=\"Standard\"" " style:class=\"text\">\n" "<style:paragraph-properties" " fo:margin-top=\"0.423cm\"" " fo:margin-bottom=\"0.212cm\"" " style:contextual-spacing=\"false\"" " fo:keep-with-next=\"always\"/>\n" "<style:text-properties" " style:font-name=\"Liberation Sans\"" " fo:font-family=\"'Liberation Sans'\"" " style:font-family-generic=\"swiss\"" " style:font-pitch=\"variable\"" " fo:font-size=\"14pt\"" " style:font-name-asian=\"Liberation Sans\"" " style:font-family-asian=\"'Liberation Sans'\"" " style:font-family-generic-asian=\"system\"" " style:font-pitch-asian=\"variable\"" " style:font-size-asian=\"14pt\"" " style:font-name-complex=\"Liberation Sans\"" " style:font-family-complex=\"'Liberation Sans'\"" " style:font-family-generic-complex=\"system\"" " style:font-pitch-complex=\"variable\"" " style:font-size-complex=\"14pt\"/>\n" "</style:style>\n")) return 0; /* Unordered list. */ if (!HBUF_PUTSL(ob, "<style:style" " style:name=\"Bullet_20_Symbols\"" " style:display-name=\"Bullet Symbols\"" " style:family=\"text\">\n" "<style:text-properties" " style:font-name=\"OpenSymbol\"" " fo:font-family=\"OpenSymbol\"" " style:font-charset=\"x-symbol\"" " style:font-name-asian=\"OpenSymbol\"" " style:font-family-asian=\"OpenSymbol\"" " style:font-charset-asian=\"x-symbol\"" " style:font-name-complex=\"OpenSymbol\"" " style:font-family-complex=\"OpenSymbol\"" " style:font-charset-complex=\"x-symbol\"/>\n" "</style:style>\n")) return 0; /* Ordered list. */ if (!HBUF_PUTSL(ob, "<style:style" " style:name=\"Numbering_20_Symbols\"" " style:display-name=\"Numbering Symbols\"" " style:family=\"text\"/>\n")) return 0; /* Headers. */ if (!HBUF_PUTSL(ob, "<style:style" " style:name=\"Heading_20_1\"" " style:display-name=\"Heading 1\"" " style:family=\"paragraph\"" " style:parent-style-name=\"Heading\"" " style:next-style-name=\"Text_20_body\"" " style:default-outline-level=\"1\"" " style:class=\"text\">\n" "<style:paragraph-properties" " fo:margin-top=\"0.423cm\"" " fo:margin-bottom=\"0.212cm\"" " style:contextual-spacing=\"false\"/>\n" "<style:text-properties" " fo:font-size=\"130%\"" " fo:font-weight=\"bold\"" " style:font-size-asian=\"130%\"" " style:font-weight-asian=\"bold\"" " style:font-size-complex=\"130%\"" " style:font-weight-complex=\"bold\"/>\n" "</style:style>\n")) return 0; if (!HBUF_PUTSL(ob, "<style:style" " style:name=\"Heading_20_2\"" " style:display-name=\"Heading 2\"" " style:family=\"paragraph\"" " style:parent-style-name=\"Heading\"" " style:next-style-name=\"Text_20_body\"" " style:default-outline-level=\"2\"" " style:class=\"text\">\n" "<style:paragraph-properties" " fo:margin-top=\"0.353cm\"" " fo:margin-bottom=\"0.212cm\"" " style:contextual-spacing=\"false\"/>\n" "<style:text-properties" " fo:font-size=\"115%\"" " fo:font-weight=\"bold\"" " style:font-size-asian=\"115%\"" " style:font-weight-asian=\"bold\"" " style:font-size-complex=\"115%\"" " style:font-weight-complex=\"bold\"/>\n" "</style:style>\n")) return 0; if (!HBUF_PUTSL(ob, "<style:style" " style:name=\"Heading_20_3\"" " style:display-name=\"Heading 3\"" " style:family=\"paragraph\"" " style:parent-style-name=\"Heading\"" " style:next-style-name=\"Text_20_body\"" " style:default-outline-level=\"3\"" " style:class=\"text\">\n" "<style:paragraph-properties" " fo:margin-top=\"0.247cm\"" " fo:margin-bottom=\"0.212cm\"" " style:contextual-spacing=\"false\"/>\n" "<style:text-properties" " fo:font-size=\"101%\"" " fo:font-weight=\"bold\"" " style:font-size-asian=\"101%\"" " style:font-weight-asian=\"bold\"" " style:font-size-complex=\"101%\"" " style:font-weight-complex=\"bold\"/>\n" "</style:style>\n")) return 0; /* Table frames. */ if (!HBUF_PUTSL(ob, "<style:style style:name=\"fr1\"" " style:family=\"graphic\"" " style:parent-style-name=\"Frame\">\n" "<style:graphic-properties" " style:run-through=\"foreground\"" " style:wrap=\"parallel\"" " style:number-wrapped-paragraphs=\"no-limit\"" " style:vertical-pos=\"middle\"" " style:vertical-rel=\"baseline\"" " style:horizontal-pos=\"center\"" " style:horizontal-rel=\"paragraph\"/>\n" " </style:style>\n")) return 0; return HBUF_PUTSL(ob, "</office:styles>\n"); } /* * Flush out the elements for scripts and styles. Return FALSE on * failure, TRUE on success. */ static int odt_styles_flush(struct lowdown_buf *ob, const struct odt *st) { size_t i; if ((st->flags & LOWDOWN_STANDALONE) && !odt_styles_flush_fixed(ob, st)) return 0; if (!HBUF_PUTSL(ob, "<office:automatic-styles>\n")) return 0; for (i = 0; i < st->stysz; i++) if (!odt_sty_flush(ob, st, &st->stys[i])) return 0; /* * I'm not sure why the page layout goes into the automatic * styles and not the fixed styles, but if placed in fixed * styles, this isn't processed. */ if (!HBUF_PUTSL(ob, "<style:page-layout style:name=\"pm1\">\n" "<style:page-layout-properties" " fo:page-width=\"21.001cm\"" " fo:page-height=\"29.7cm\"" " style:num-format=\"1\"" " style:print-orientation=\"portrait\"" " fo:margin-top=\"2cm\"" " fo:margin-bottom=\"2cm\"" " fo:margin-left=\"2cm\"" " fo:margin-right=\"2cm\"" " style:writing-mode=\"lr-tb\"" " style:footnote-max-height=\"0cm\">\n" "</style:page-layout-properties>\n" "</style:page-layout>\n")) return 0; if (!HBUF_PUTSL(ob, "</office:automatic-styles>\n")) return 0; /* * Since this references an automatic style (pm1), emit this * regardless of whether we're in standalone or not. */ return HBUF_PUTSL(ob, "<office:master-styles>\n" "<style:master-page " " style:name=\"Standard\"" " style:page-layout-name=\"pm1\"/>\n" "</office:master-styles>\n"); } /* * Use our metadata to grab change identifiers. Return FALSE on * failure, TRUE on success. */ static int odt_changes_flush(struct lowdown_buf *ob, const struct lowdown_metaq *mq, const struct odt *st) { const struct lowdown_meta *m; const char *author = NULL, *date = NULL, *rcsauthor = NULL, *rcsdate = NULL; char buf[64]; size_t i; time_t t = time(NULL); if (st->chngsz == 0) return 1; TAILQ_FOREACH(m, mq, entries) if (strcasecmp(m->key, "author") == 0) author = m->value; else if (strcasecmp(m->key, "date") == 0) date = m->value; else if (strcasecmp(m->key, "rcsauthor") == 0) rcsauthor = rcsauthor2str(m->value); else if (strcasecmp(m->key, "rcsdate") == 0) rcsdate = rcsdate2str(m->value); /* Overrides. */ if (rcsdate != NULL) date = rcsdate; if (rcsauthor != NULL) author = rcsauthor; /* We require at least a date. */ if (date == NULL) { if (strftime(buf, sizeof(buf), "%Y-%m-%dT%H:%M:%S", localtime(&t)) == 0) date = "1970-01-01"; else date = buf; } if (!HBUF_PUTSL(ob, "<text:tracked-changes" " text:track-changes=\"false\">\n")) return 0; for (i = 0; i < st->chngsz; i++) { if (!hbuf_printf(ob, "<text:changed-region" " xml:id=\"ct%zu\"" " text:id=\"ct%zu\">\n" "<text:%s>\n" "<office:change-info>\n", i, i, st->chngs[i].ins ? "insertion" : "deletion")) return 0; if (author != NULL) { if (!HBUF_PUTSL(ob, "<dc:creator>")) return 0; if (!hesc_html(ob, author, strlen(author), 1, 0, 1)) return 0; if (!HBUF_PUTSL(ob, "</dc:creator>\n")) return 0; } if (!HBUF_PUTSL(ob, "<dc:date>")) return 0; if (!hesc_html(ob, date, strlen(date), 1, 0, 1)) return 0; if (!HBUF_PUTSL(ob, "</dc:date>\n")) return 0; if (!hbuf_printf(ob, "</office:change-info>\n" "</text:%s>\n" "</text:changed-region>\n", st->chngs[i].ins ? "insertion" : "deletion")) return 0; } return HBUF_PUTSL(ob, "</text:tracked-changes>\n"); } /* * Flush out the <office:meta> element, if applicable. Return FALSE on * failure, TRUE on success. */ static int odt_metaq_flush(struct lowdown_buf *ob, const struct lowdown_metaq *mq, const struct odt *st) { const struct lowdown_meta *m; const char *author = NULL, *title = NULL, *date = NULL, *rcsauthor = NULL, *rcsdate = NULL; if (mq == NULL || TAILQ_EMPTY(mq)) return 1; TAILQ_FOREACH(m, mq, entries) if (strcasecmp(m->key, "author") == 0) author = m->value; else if (strcasecmp(m->key, "date") == 0) date = m->value; else if (strcasecmp(m->key, "rcsauthor") == 0) rcsauthor = rcsauthor2str(m->value); else if (strcasecmp(m->key, "rcsdate") == 0) rcsdate = rcsdate2str(m->value); else if (strcasecmp(m->key, "title") == 0) title = m->value; /* Overrides. */ if (title == NULL) title = "Untitled article"; if (rcsdate != NULL) date = rcsdate; if (rcsauthor != NULL) author = rcsauthor; if (!HBUF_PUTSL(ob, "<office:meta>\n")) return 0; if (!HBUF_PUTSL(ob, "<dc:title>")) return 0; if (!hesc_html(ob, title, strlen(title), 1, 0, 1)) return 0; if (!HBUF_PUTSL(ob, "</dc:title>\n")) return 0; if (author != NULL) { if (!HBUF_PUTSL(ob, "<dc:creator>")) return 0; if (!hesc_html(ob, author, strlen(author), 1, 0, 1)) return 0; if (!HBUF_PUTSL(ob, "</dc:creator>\n")) return 0; if (!HBUF_PUTSL(ob, "<meta:initial-creator>")) return 0; if (!hesc_html(ob, author, strlen(author), 1, 0, 1)) return 0; if (!HBUF_PUTSL(ob, "</meta:initial-creator>\n")) return 0; } if (date != NULL) { if (!HBUF_PUTSL(ob, "<dc:date>")) return 0; if (!hesc_html(ob, date, strlen(date), 1, 0, 1)) return 0; if (!HBUF_PUTSL(ob, "</dc:date>\n")) return 0; if (!HBUF_PUTSL(ob, "<meta:creation-date>")) return 0; if (!hesc_html(ob, date, strlen(date), 1, 0, 1)) return 0; if (!HBUF_PUTSL(ob, "</meta:creation-date>\n")) return 0; } return HBUF_PUTSL(ob, "</office:meta>\n"); } /* * Escape regular text that shouldn't be HTML. Return FALSE on failure, * TRUE on success. */ static int escape_html(struct lowdown_buf *ob, const char *source, size_t length, const struct odt *st) { return hesc_html(ob, source, length, 1, 0, 1); } /* * See escape_html(). */ static int escape_htmlb(struct lowdown_buf *ob, const struct lowdown_buf *in, const struct odt *st) { return escape_html(ob, in->data, in->size, st); } /* * Escape an href link. Return FALSE on failure, TRUE on success. */ static int escape_href(struct lowdown_buf *ob, const struct lowdown_buf *in, const struct odt *st) { return hesc_href(ob, in->data, in->size); } static int escape_attr(struct lowdown_buf *ob, const struct lowdown_buf *in) { return hesc_attr(ob, in->data, in->size); } /* * Return FALSE on failure, TRUE on success. */ static int rndr_autolink(struct lowdown_buf *ob, const struct rndr_autolink *parm, struct odt *st) { if (parm->link.size == 0) return 1; if (!HBUF_PUTSL(ob, "<text:a xlink:type=\"simple\"" " text:style-name=\"Internet_20_Link\" xlink:href=\"")) return 0; if (parm->type == HALINK_EMAIL && !HBUF_PUTSL(ob, "mailto:")) return 0; if (!escape_href(ob, &parm->link, st)) return 0; if (!HBUF_PUTSL(ob, "\">")) return 0; /* * Pretty printing: if we get an email address as * an actual URI, e.g. `mailto:foo@bar.com`, we don't * want to print the `mailto:` prefix */ if (hbuf_strprefix(&parm->link, "mailto:")) { if (!escape_html(ob, parm->link.data + 7, parm->link.size - 7, st)) return 0; } else { if (!escape_htmlb(ob, &parm->link, st)) return 0; } return HBUF_PUTSL(ob, "</text:a>"); } /* * Return FALSE on failure, TRUE on success. */ static int rndr_blockcode(struct lowdown_buf *ob, const struct rndr_blockcode *parm, struct odt *st) { size_t i, j, sz, ssz; struct odt_sty *s; if (ob->size && !hbuf_putc(ob, '\n')) return 0; for (i = 0; i < st->stysz; i++) if (st->stys[i].type == LOWDOWN_PARAGRAPH && st->stys[i].fmt == ODT_STY_LIT && st->stys[i].parent == st->list && st->stys[i].offs == st->offs) break; if (i == st->stysz) { if ((s = odt_style_add(st)) == NULL) return 0; s->type = LOWDOWN_PARAGRAPH; s->fmt = ODT_STY_LIT; s->parent = st->list; s->offs = st->offs; snprintf(s->name, sizeof(s->name), "P%zu", st->sty_P++); } else s = &st->stys[i]; for (i = 0; i < parm->text.size; ) { if (!hbuf_printf(ob, "<text:p text:style-name=\"%s\">", s->name)) return 0; /* * Iterate through each line, printing it in its own * <text:p>. If we encounter more than one space in a * row, then use a <text:s text:c> spanner to print the * literal spaces. */ for (sz = 0, j = i; i < parm->text.size; i++, sz++) { if (parm->text.data[i] == ' ' && i < parm->text.size - 1 && parm->text.data[i + 1] == ' ') { if (!hesc_html(ob, &parm->text.data[j], sz, 1, 1, 1)) return 0; sz = 0; for (ssz = 0; i < parm->text.size; i++, ssz++) if (parm->text.data[i] != ' ') break; j = i; if (!hbuf_printf(ob, "<text:s text:c=\"%zu\"/>", ssz)) return 0; } if (i < parm->text.size && parm->text.data[i] == '\n') break; } if (!hesc_html(ob, &parm->text.data[j], sz, 1, 1, 1)) return 0; if (!HBUF_PUTSL(ob, "</text:p>\n")) return 0; if (i < parm->text.size) i++; } return 1; } /* * Return FALSE on failure, TRUE on success. */ static int rndr_codespan(struct lowdown_buf *ob, const struct rndr_codespan *param, struct odt *st) { if (!HBUF_PUTSL(ob, "<text:span text:style-name=\"Source_20_Text\">")) return 0; if (!escape_htmlb(ob, ¶m->text, st)) return 0; return HBUF_PUTSL(ob, "</text:span>"); } /* * This covers all manner of span types: italic, bold, etc. Return * FALSE on failure, TRUE on success. */ static int rndr_span(struct lowdown_buf *ob, const struct lowdown_buf *content, const struct lowdown_node *n, struct odt *st) { const char *sty; if ((sty = odt_style_add_text(st, n->type)) == NULL) return 0; if (!hbuf_printf(ob, "<text:span text:style-name=\"%s\">", sty)) return 0; if (!hbuf_putb(ob, content)) return 0; return HBUF_PUTSL(ob, "</text:span>"); } /* * Return FALSE on failure, TRUE on success. */ static int rndr_linebreak(struct lowdown_buf *ob) { return HBUF_PUTSL(ob, "<text:line-break/>\n"); } /* * Return FALSE on failure, TRUE on success. */ static int rndr_header(struct lowdown_buf *ob, const struct lowdown_buf *content, const struct lowdown_node *n, struct odt *st) { struct odt_sty *sty; ssize_t level; size_t i; int fl, rc = 0; const struct lowdown_buf *buf; struct lowdown_buf *nbuf = NULL; level = (ssize_t)n->rndr_header.level + st->headers_offs; if (level < 1) level = 1; else if (level > 3) level = 3; if (level == 1) fl = ODT_STY_H1; else if (level == 2) fl = ODT_STY_H2; else fl = ODT_STY_H3; for (i = 0; i < st->stysz; i++) if (st->stys[i].type == LOWDOWN_HEADER && st->stys[i].fmt == fl) break; if (i == st->stysz) { if ((sty = odt_style_add(st)) == NULL) return 0; sty->fmt = fl; sty->type = LOWDOWN_HEADER; snprintf(sty->name, sizeof(sty->name), "P%zu", st->sty_P++); } else sty = &st->stys[i]; if (ob->size && !hbuf_putc(ob, '\n')) return 0; if (!hbuf_printf(ob, "<text:h" " text:outline-level=\"%zu\"" " text:style-name=\"%s\"", level, sty->name)) return 0; if (n->rndr_header.attr_cls.size > 0) { if (!HBUF_PUTSL(ob, " text:class-names=\"")) return 0; if (!hbuf_putb(ob, &n->rndr_header.attr_cls)) return 0; if (!HBUF_PUTSL(ob, "\"")) return 0; } if (!HBUF_PUTSL(ob, ">")) return 0; if (n->rndr_header.attr_id.size) { if ((nbuf = hbuf_new(32)) == NULL) goto out; if (!escape_href(nbuf, &n->rndr_header.attr_id, st)) goto out; buf = nbuf; } else buf = hbuf_id(NULL, n, &st->headers_used); if (buf == NULL) goto out; if (!HBUF_PUTSL(ob, "<text:bookmark-start text:name=\"")) goto out; if (!hbuf_putb(ob, buf)) goto out; if (!HBUF_PUTSL(ob, "\" />")) goto out; if (!hbuf_putb(ob, content)) goto out; if (!HBUF_PUTSL(ob, "<text:bookmark-end text:name=\"")) goto out; if (!hbuf_putb(ob, buf)) goto out; if (!HBUF_PUTSL(ob, "\" />")) goto out; if (!HBUF_PUTSL(ob, "</text:h>\n")) goto out; rc = 1; out: hbuf_free(nbuf); return rc; } /* * Return FALSE on failure, TRUE on success. */ static int rndr_link(struct lowdown_buf *ob, const struct lowdown_buf *content, const struct rndr_link *param, struct odt *st) { if (param->attr_id.size > 0) { if (!HBUF_PUTSL(ob, "<text:bookmark-start text:name=\"")) return 0; if (!hbuf_putb(ob, ¶m->attr_id)) return 0; if (!HBUF_PUTSL(ob, "\" />")) return 0; } if (!HBUF_PUTSL(ob, "<text:a" " xlink:type=\"simple\"" " text:style-name=\"Internet_20_Link\"")) return 0; if (param->attr_cls.size > 0) { if (!HBUF_PUTSL(ob, " text:class-names=\"")) return 0; if (!hbuf_putb(ob, ¶m->attr_cls)) return 0; if (!HBUF_PUTSL(ob, "\"")) return 0; } if (!HBUF_PUTSL(ob, " xlink:href=\"")) return 0; if (!escape_href(ob, ¶m->link, st)) return 0; if (!HBUF_PUTSL(ob, "\">") || !hbuf_putb(ob, content) || !HBUF_PUTSL(ob, "</text:a>")) return 0; if (param->attr_id.size > 0) { if (!HBUF_PUTSL(ob, "<text:bookmark-end text:name=\"")) return 0; if (!hbuf_putb(ob, ¶m->attr_id)) return 0; if (!HBUF_PUTSL(ob, "\" />")) return 0; } return 1; } /* * Return FALSE on failure, TRUE on success. */ static int rndr_list(struct lowdown_buf *ob, const struct lowdown_buf *content, const struct rndr_list *param, const char *name) { if (ob->size && !hbuf_putc(ob, '\n')) return 0; if (!HBUF_PUTSL(ob, "<text:list")) return 0; if (name != NULL && !hbuf_printf(ob, " text:style-name=\"%s\"", name)) return 0; if (!HBUF_PUTSL(ob, ">\n")) return 0; if (!hbuf_putb(ob, content)) return 0; return HBUF_PUTSL(ob, "</text:list>\n"); } /* * Return FALSE on failure, TRUE on success. */ static int rndr_listitem(struct lowdown_buf *ob, const struct lowdown_buf *content, const struct lowdown_node *n, struct odt *st) { size_t i, size; struct odt_sty *sty; if (!(n->rndr_listitem.flags & HLIST_FL_DEF)) { assert(st->list != (size_t)-1); if (!HBUF_PUTSL(ob, "<text:list-item>")) return 0; } /* * Non-definition, non-block lists have an initial paragraph * that must link to the root list of the current tree. */ if (!(n->rndr_listitem.flags & HLIST_FL_DEF) && !(n->rndr_listitem.flags & HLIST_FL_BLOCK)) { assert(st->list != (size_t)-1); for (i = 0; i < st->stysz; i++) if (st->stys[i].type == LOWDOWN_PARAGRAPH && st->stys[i].fmt == ODT_STY_PARA && st->stys[i].foot == st->foot && st->stys[i].parent == st->list) break; if (i == st->stysz) { if ((sty = odt_style_add(st)) == NULL) return 0; sty->parent = st->list; sty->foot = st->foot; sty->fmt = ODT_STY_PARA; sty->type = LOWDOWN_PARAGRAPH; snprintf(sty->name, sizeof(sty->name), "P%zu", st->sty_P++); } else sty = &st->stys[i]; if (!hbuf_printf(ob, "<text:p text:style-name=\"%s\">", sty->name)) return 0; } if (n->rndr_listitem.flags & HLIST_FL_UNCHECKED) { if (!HBUF_PUTSL(ob, "☐ ")) return 0; } if (n->rndr_listitem.flags & HLIST_FL_CHECKED) { if (!HBUF_PUTSL(ob, "☑ ")) return 0; } /* Cut off any trailing space. */ if ((size = content->size) > 0) { while (size && content->data[size - 1] == '\n') size--; if (!hbuf_put(ob, content->data, size)) return 0; } if (!(n->rndr_listitem.flags & HLIST_FL_DEF) && !(n->rndr_listitem.flags & HLIST_FL_BLOCK)) if (!HBUF_PUTSL(ob, "</text:p>")) return 0; if (!(n->rndr_listitem.flags & HLIST_FL_DEF)) if (!HBUF_PUTSL(ob, "</text:list-item>\n")) return 0; return 1; } /* * Return FALSE on failure, TRUE on success. */ static int rndr_paragraph(struct lowdown_buf *ob, const struct lowdown_buf *content, struct odt *st) { size_t i = 0, j; struct odt_sty *sty; if (content->size == 0) return 1; while (i < content->size && isspace((unsigned char)content->data[i])) i++; if (i == content->size) return 1; /* * Paragraphs need to either set their left margin, if in * blockquotes, or link to the root list, if applicable. The * foot bits are because footer paragraphs inherit the footnote * font. */ for (j = 0; j < st->stysz; j++) if (st->stys[j].type == LOWDOWN_PARAGRAPH && st->stys[j].parent == st->list && st->stys[j].foot == st->foot && st->stys[j].fmt == ODT_STY_PARA && st->stys[j].offs == st->offs) break; if (j == st->stysz) { if ((sty = odt_style_add(st)) == NULL) return 0; sty->foot = st->foot; sty->fmt = ODT_STY_PARA; sty->type = LOWDOWN_PARAGRAPH; sty->parent = st->list; sty->offs = st->offs; snprintf(sty->name, sizeof(sty->name), "P%zu", st->sty_P++); } else sty = &st->stys[j]; if (ob->size && !hbuf_putc(ob, '\n')) return 0; if (!hbuf_printf(ob, "<text:p text:style-name=\"%s\">", sty->name)) return 0; if (!hbuf_put(ob, content->data + i, content->size - i)) return 0; return HBUF_PUTSL(ob, "</text:p>\n"); } /* * Return FALSE on failure, TRUE on success. */ static int rndr_html(struct lowdown_buf *ob, const struct lowdown_buf *param, const struct odt *st) { if (st->flags & LOWDOWN_ODT_SKIP_HTML) return 1; return escape_htmlb(ob, param, st); } /* * Return FALSE on failure, TRUE on success. */ static int rndr_hrule(struct lowdown_buf *ob, struct odt *st) { if (ob->size && !hbuf_putc(ob, '\n')) return 0; return HBUF_PUTSL(ob, "<text:p text:style-name=\"Horizontal_20_Line\"/>\n"); } static int rndr_image(struct lowdown_buf *ob, const struct rndr_image *param, const struct odt *st) { unsigned int x = 0, y = 0; char dimbuf[32]; /* * Scan in our dimensions, if applicable. * It's unreasonable for them to be over 32 characters, so use * that as a cap to the size. */ if (param->dims.size && param->dims.size < sizeof(dimbuf) - 1) { memset(dimbuf, 0, sizeof(dimbuf)); memcpy(dimbuf, param->dims.data, param->dims.size); if (sscanf(dimbuf, "%ux%u", &x, &y) != 2) x = y = 0; } if (!HBUF_PUTSL(ob, "<draw:frame" " draw:name=\"Image1\"" " text:anchor-type=\"as-char\"" " draw:z-index=\"0\"" " draw:style-name=\"Graphics\"")) return 0; if (param->attr_cls.size > 0) { if (!HBUF_PUTSL(ob, " draw:class-names=\"")) return 0; if (!hbuf_putb(ob, ¶m->attr_cls)) return 0; if (!HBUF_PUTSL(ob, "\"")) return 0; } if (param->attr_width.size || param->attr_height.size) { if (param->attr_width.size) if (!HBUF_PUTSL(ob, " svg:width=\"") || !escape_attr(ob, ¶m->attr_width) || !HBUF_PUTSL(ob, "\"")) return 0; if (param->attr_height.size) if (!HBUF_PUTSL(ob, " svg:height=\"") || !escape_attr(ob, ¶m->attr_height) || !HBUF_PUTSL(ob, "\"")) return 0; } else if (x > 0 && y > 0) { if (!hbuf_printf(ob, " svg:width=\"%u px\"" " svg:height=\"%u px\"", x, y)) return 0; } if (!HBUF_PUTSL(ob, "><draw:image xlink:href=\"")) return 0; if (!hbuf_putb(ob, ¶m->link)) return 0; if (!HBUF_PUTSL(ob, "\"" " xlink:type=\"simple\"" " xlink:show=\"embed\"" " xlink:actuate=\"onLoad\"" " draw:filter-name=\"<All images>\" />")) return 0; if (!HBUF_PUTSL(ob, "<svg:title>")) return 0; if (!hbuf_putb(ob, ¶m->alt)) return 0; return HBUF_PUTSL(ob, "</svg:title></draw:frame>"); } /* * Return FALSE on failure, TRUE on success. */ static int rndr_table(struct lowdown_buf *ob, const struct lowdown_buf *content, const struct rndr_table *param, struct odt *st) { size_t i, pid; struct odt_sty *s; /* * First find the outer paragraph. If we're in the footer, this * must be linked to the footer; and if in a list, to the list. * We don't do offset here: that's part of the table itself. */ for (pid = 0; pid < st->stysz; pid++) if (st->stys[pid].type == LOWDOWN_PARAGRAPH && st->stys[pid].fmt == ODT_STY_PARA && st->stys[pid].offs == 0 && st->stys[pid].foot == st->foot && st->stys[pid].parent == st->list) break; if (pid == st->stysz) { if ((s = odt_style_add(st)) == NULL) return 0; s->parent = st->list; s->foot = st->foot; s->fmt = ODT_STY_PARA; s->type = LOWDOWN_PARAGRAPH; snprintf(s->name, sizeof(s->name), "P%zu", st->sty_P++); } /* * Now the table itself. Tables are only unique insofar as they * have different offsets and possible are in lists. */ for (i = 0; i < st->stysz; i++) if (st->stys[i].type == LOWDOWN_TABLE_BLOCK && st->stys[i].parent == st->list && st->stys[i].foot == st->foot && st->stys[i].offs == st->offs) break; if (i == st->stysz) { if ((s = odt_style_add(st)) == NULL) return 0; s->type = LOWDOWN_TABLE_BLOCK; s->fmt = ODT_STY_TBL; s->foot = st->foot; s->parent = st->list; s->offs = st->offs; snprintf(s->name, sizeof(s->name), "Table%zu", st->sty_Table++); } else s = &st->stys[i]; if (ob->size && !hbuf_putc(ob, '\n')) return 0; if (!hbuf_printf(ob, "<text:p text:style-name=\"%s\">\n", st->stys[pid].name)) return 0; if (!hbuf_printf(ob, "<draw:frame draw:style-name=\"fr1\"" " draw:name=\"Frame\"" " draw:z-index=\"0\">\n" "<draw:text-box" " fo:min-height=\"0.499cm\"" " fo:min-width=\"0.34cm\">\n" "<table:table" " table:style-name=\"%s\"" " table:name=\"%s\">\n" "<table:table-column" " table:number-columns-repeated=\"%zu\"/>\n", s->name, s->name, param->columns)) return 0; if (!hbuf_putb(ob, content)) return 0; if (!HBUF_PUTSL(ob, "</table:table>\n")) return 0; if (!hbuf_printf(ob, "</draw:text-box>\n</draw:frame>\n</text:p>\n")) return 0; return 1; } /* * Return FALSE on failure, TRUE on success. */ static int rndr_tablerow(struct lowdown_buf *ob, const struct lowdown_buf *content) { if (!HBUF_PUTSL(ob, "<table:table-row>\n")) return 0; if (!hbuf_putb(ob, content)) return 0; return HBUF_PUTSL(ob, "</table:table-row>\n"); } /* * Return FALSE on failure, TRUE on success. */ static int rndr_tablecell(struct lowdown_buf *ob, const struct lowdown_buf *content, const struct rndr_table_cell *param, struct odt *st) { size_t i; struct odt_sty *s; /* * Reference if we're in a footnote, as the paragraph will want * to inherit the Footnote smaller font. */ for (i = 0; i < st->stysz; i++) if (st->stys[i].type == LOWDOWN_PARAGRAPH && st->stys[i].foot == st->foot && st->stys[i].fmt == ODT_STY_TBL_PARA) break; if (i == st->stysz) { if ((s = odt_style_add(st)) == NULL) return 0; s->type = LOWDOWN_PARAGRAPH; s->foot = st->foot; s->fmt = ODT_STY_TBL_PARA; snprintf(s->name, sizeof(s->name), "P%zu", st->sty_P++); } else s = &st->stys[i]; if (!hbuf_printf(ob, "<table:table-cell office:value-type=\"string\">" "<text:p text:style-name=\"%s\">", s->name)) return 0; if (!hbuf_putb(ob, content)) return 0; return HBUF_PUTSL(ob, "</text:p></table:table-cell>\n"); } /* * Return FALSE on failure, TRUE on success. */ static int rndr_footnote_ref(struct lowdown_buf *ob, const struct lowdown_buf *content, struct odt *st) { struct odt tmp; /* Save state values. */ tmp = *st; st->offs = 0; st->list = (size_t)-1; st->foot = 1; st->footcount++; if (!hbuf_printf(ob, "<text:note text:id=\"ftn%zu\"" " text:note-class=\"footnote\">" "<text:note-citation>%zu</text:note-citation>" "<text:note-body>\n", st->footcount, st->footcount)) return 0; if (!hbuf_putb(ob, content)) return 0; if (!HBUF_PUTSL(ob, "</text:note-body></text:note>\n")) return 0; /* Restore state values. */ st->offs = tmp.offs; st->list = tmp.list; st->foot = 0; return 1; } /* * Return FALSE on failure, TRUE on success. */ static int rndr_math(struct lowdown_buf *ob, const struct rndr_math *param, const struct odt *st) { if (param->blockmode && !HBUF_PUTSL(ob, "\\[")) return 0; else if (!param->blockmode && !HBUF_PUTSL(ob, "\\(")) return 0; if (!escape_htmlb(ob, ¶m->text, st)) return 0; return param->blockmode ? HBUF_PUTSL(ob, "\\]") : HBUF_PUTSL(ob, "\\)"); } /* * Return FALSE on failure, TRUE on success. */ static int rndr_root(struct lowdown_buf *ob, const struct lowdown_metaq *mq, const struct lowdown_buf *content, const struct odt *st) { if ((st->flags & LOWDOWN_STANDALONE) && !HBUF_PUTSL(ob, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" "<office:document\n" " xmlns:calcext=\"urn:org:documentfoundation:names:experimental:calc:xmlns:calcext:1.0\"\n" " xmlns:chart=\"urn:oasis:names:tc:opendocument:xmlns:chart:1.0\"\n" " xmlns:config=\"urn:oasis:names:tc:opendocument:xmlns:config:1.0\"\n" " xmlns:css3t=\"http://www.w3.org/TR/css3-text/\"\n" " xmlns:dc=\"http://purl.org/dc/elements/1.1/\"\n" " xmlns:dom=\"http://www.w3.org/2001/xml-events\"\n" " xmlns:dr3d=\"urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0\"\n" " xmlns:draw=\"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0\"\n" " xmlns:drawooo=\"http://openoffice.org/2010/draw\"\n" " xmlns:field=\"urn:openoffice:names:experimental:ooo-ms-interop:xmlns:field:1.0\"\n" " xmlns:fo=\"urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0\"\n" " xmlns:form=\"urn:oasis:names:tc:opendocument:xmlns:form:1.0\"\n" " xmlns:formx=\"urn:openoffice:names:experimental:ooxml-odf-interop:xmlns:form:1.0\"\n" " xmlns:grddl=\"http://www.w3.org/2003/g/data-view#\"\n" " xmlns:loext=\"urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0\"\n" " xmlns:math=\"http://www.w3.org/1998/Math/MathML\"\n" " xmlns:meta=\"urn:oasis:names:tc:opendocument:xmlns:meta:1.0\"\n" " xmlns:number=\"urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0\"\n" " xmlns:of=\"urn:oasis:names:tc:opendocument:xmlns:of:1.2\"\n" " xmlns:office=\"urn:oasis:names:tc:opendocument:xmlns:office:1.0\"\n" " xmlns:officeooo=\"http://openoffice.org/2009/office\"\n" " xmlns:ooo=\"http://openoffice.org/2004/office\"\n" " xmlns:oooc=\"http://openoffice.org/2004/calc\"\n" " xmlns:ooow=\"http://openoffice.org/2004/writer\"\n" " xmlns:rpt=\"http://openoffice.org/2005/report\"\n" " xmlns:script=\"urn:oasis:names:tc:opendocument:xmlns:script:1.0\"\n" " xmlns:style=\"urn:oasis:names:tc:opendocument:xmlns:style:1.0\"\n" " xmlns:svg=\"urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0\"\n" " xmlns:table=\"urn:oasis:names:tc:opendocument:xmlns:table:1.0\"\n" " xmlns:tableooo=\"http://openoffice.org/2009/table\"\n" " xmlns:text=\"urn:oasis:names:tc:opendocument:xmlns:text:1.0\"\n" " xmlns:xforms=\"http://www.w3.org/2002/xforms\"\n" " xmlns:xhtml=\"http://www.w3.org/1999/xhtml\"\n" " xmlns:xlink=\"http://www.w3.org/1999/xlink\"\n" " xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"\n" " xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n" " office:mimetype=\"application/vnd.oasis.opendocument.text\"\n" " office:version=\"1.3\">\n")) return 0; if ((st->flags & LOWDOWN_STANDALONE) && !odt_metaq_flush(ob, mq, st)) return 0; if (!odt_styles_flush(ob, st)) return 0; if (!HBUF_PUTSL(ob, "<office:body>\n<office:text>\n")) return 0; if (!odt_changes_flush(ob, mq, st)) return 0; if (!hbuf_putb(ob, content)) return 0; if (!HBUF_PUTSL(ob, "</office:text>\n</office:body>\n")) return 0; if ((st->flags & LOWDOWN_STANDALONE) && !HBUF_PUTSL(ob, "</office:document>\n")) return 0; return 1; } /* * Allocate a meta-data value on the queue "mq". Return FALSE on * failure, TRUE on success. */ static int rndr_meta(struct lowdown_buf *ob, const struct lowdown_buf *content, struct lowdown_metaq *mq, const struct lowdown_node *n, struct odt *st) { struct lowdown_meta *m; ssize_t val; const char *ep; m = calloc(1, sizeof(struct lowdown_meta)); if (m == NULL) return 0; TAILQ_INSERT_TAIL(mq, m, entries); m->key = strndup(n->rndr_meta.key.data, n->rndr_meta.key.size); if (m->key == NULL) return 0; m->value = strndup(content->data, content->size); if (m->value == NULL) return 0; if (strcmp(m->key, "shiftheadinglevelby") == 0) { val = (ssize_t)strtonum (m->value, -100, 100, &ep); if (ep == NULL) st->headers_offs = val + 1; } else if (strcmp(m->key, "baseheaderlevel") == 0) { val = (ssize_t)strtonum (m->value, 1, 100, &ep); if (ep == NULL) st->headers_offs = val; } return 1; } static int rndr(struct lowdown_buf *ob, struct lowdown_metaq *mq, void *ref, const struct lowdown_node *n) { const struct lowdown_node *child; struct lowdown_buf *tmp; int32_t ent; struct odt *st = ref; struct odt_sty *sty = NULL; size_t curid = (size_t)-1, curoffs, chngid = (size_t)-1; int ret = 1; void *pp; if ((tmp = hbuf_new(64)) == NULL) return 0; /* * Manage our position in the output. If we're in a blockquote * and not a list, then increment our indent. If we're in a * list, we're not allowed to have indents between the list and * content (OpenDocument limitations), so don't touch the * indentation. */ /* * TODO: keep a "real offset" if we have an embedded table and * want to set the width to be the real offset minus page width. * Without doing so, list-embedded tables run off the right * margin for OpenDocument reasons. */ switch (n->type) { case LOWDOWN_DEFINITION_DATA: case LOWDOWN_BLOCKQUOTE: if (st->list == (size_t)-1) st->offs++; break; case LOWDOWN_LIST: if (st->list != (size_t)-1) break; for (st->list = 0; st->list < st->stysz; st->list++) { if (st->stys[st->list].type != LOWDOWN_LIST) continue; if (st->stys[st->list].offs != st->offs) continue; if ((n->rndr_list.flags & HLIST_FL_UNORDERED) && st->stys[st->list].fmt != ODT_STY_UL) continue; if ((n->rndr_list.flags & HLIST_FL_ORDERED) && st->stys[st->list].fmt != ODT_STY_OL) continue; break; } if (st->list == st->stysz) { if ((sty = odt_style_add(st)) == NULL) return 0; sty->type = LOWDOWN_LIST; if (n->rndr_list.flags & HLIST_FL_ORDERED) sty->fmt = ODT_STY_OL; if (n->rndr_list.flags & HLIST_FL_UNORDERED) sty->fmt = ODT_STY_UL; sty->offs = st->offs; snprintf(sty->name, sizeof(sty->name), "L%zu", st->sty_L++); } curoffs = st->offs; st->offs = 0; curid = st->list; break; default: break; } TAILQ_FOREACH(child, &n->children, entries) if (!rndr(tmp, mq, st, child)) goto out; if (n->chng == LOWDOWN_CHNG_INSERT || n->chng == LOWDOWN_CHNG_DELETE) { pp = reallocarray(st->chngs, st->chngsz + 1, sizeof(struct odt_chng)); if (pp == NULL) goto out; st->chngs = pp; st->chngs[st->chngsz].ins = n->chng == LOWDOWN_CHNG_INSERT; chngid = st->chngsz++; if (!hbuf_printf(ob, "<text:change-start" " text:change-id=\"ct%zu\"/>", chngid)) goto out; } switch (n->type) { case LOWDOWN_ROOT: if (!rndr_root(ob, mq, tmp, st)) goto out; break; case LOWDOWN_BLOCKCODE: if (!rndr_blockcode(ob, &n->rndr_blockcode, st)) goto out; break; case LOWDOWN_META: if (n->chng != LOWDOWN_CHNG_DELETE && !rndr_meta(ob, tmp, mq, n, st)) goto out; break; case LOWDOWN_HEADER: if (!rndr_header(ob, tmp, n, st)) goto out; break; case LOWDOWN_HRULE: if (!rndr_hrule(ob, st)) goto out; break; case LOWDOWN_LIST: if (!rndr_list(ob, tmp, &n->rndr_list, curid == (size_t)-1 ? NULL : st->stys[curid].name)) goto out; break; case LOWDOWN_LISTITEM: if (!rndr_listitem(ob, tmp, n, st)) goto out; break; case LOWDOWN_DEFINITION_TITLE: case LOWDOWN_DEFINITION_DATA: case LOWDOWN_PARAGRAPH: if (!rndr_paragraph(ob, tmp, st)) goto out; break; case LOWDOWN_TABLE_BLOCK: if (!rndr_table(ob, tmp, &n->rndr_table, st)) goto out; break; case LOWDOWN_TABLE_ROW: if (!rndr_tablerow(ob, tmp)) goto out; break; case LOWDOWN_TABLE_CELL: if (!rndr_tablecell(ob, tmp, &n->rndr_table_cell, st)) goto out; break; case LOWDOWN_BLOCKHTML: if (!rndr_html(ob, &n->rndr_blockhtml.text, st)) goto out; break; case LOWDOWN_LINK_AUTO: if (!rndr_autolink(ob, &n->rndr_autolink, st)) goto out; break; case LOWDOWN_CODESPAN: if (!rndr_codespan(ob, &n->rndr_codespan, st)) goto out; break; case LOWDOWN_TRIPLE_EMPHASIS: case LOWDOWN_DOUBLE_EMPHASIS: case LOWDOWN_EMPHASIS: case LOWDOWN_STRIKETHROUGH: case LOWDOWN_HIGHLIGHT: case LOWDOWN_SUPERSCRIPT: if (!rndr_span(ob, tmp, n, st)) goto out; break; case LOWDOWN_IMAGE: if (!rndr_image(ob, &n->rndr_image, st)) goto out; break; case LOWDOWN_LINEBREAK: if (!rndr_linebreak(ob)) goto out; break; case LOWDOWN_LINK: if (!rndr_link(ob, tmp, &n->rndr_link, st)) goto out; break; case LOWDOWN_FOOTNOTE: if (!rndr_footnote_ref(ob, tmp, st)) goto out; break; case LOWDOWN_MATH_BLOCK: if (!rndr_math(ob, &n->rndr_math, st)) goto out; break; case LOWDOWN_RAW_HTML: if (!rndr_html(ob, &n->rndr_raw_html.text, st)) goto out; break; case LOWDOWN_NORMAL_TEXT: if (!escape_htmlb(ob, &n->rndr_normal_text.text, st)) goto out; break; case LOWDOWN_ENTITY: ent = entity_find_iso(&n->rndr_entity.text); if (ent > 0 && !hbuf_printf(ob, "&#%" PRId32 ";", ent)) goto out; if (ent <= 0 && !hbuf_putb(ob, &n->rndr_entity.text)) goto out; break; default: if (!hbuf_putb(ob, tmp)) goto out; break; } if (n->chng == LOWDOWN_CHNG_INSERT || n->chng == LOWDOWN_CHNG_DELETE) { assert(chngid != (size_t)-1); if (!hbuf_printf(ob, "<text:change-end" " text:change-id=\"ct%zu\"/>", chngid)) goto out; } switch (n->type) { case LOWDOWN_DEFINITION_DATA: case LOWDOWN_BLOCKQUOTE: if (st->list == (size_t)-1) st->offs--; break; case LOWDOWN_LIST: if (curid != (size_t)-1) { st->list = (size_t)-1; st->offs = curoffs; } break; default: break; } ret = 1; out: hbuf_free(tmp); return ret; } int lowdown_odt_rndr(struct lowdown_buf *ob, void *arg, const struct lowdown_node *n) { struct odt *st = arg; struct lowdown_metaq metaq; int rc; TAILQ_INIT(&st->headers_used); TAILQ_INIT(&metaq); st->headers_offs = 1; st->stys = NULL; st->stysz = 0; st->list = (size_t)-1; st->foot = 0; st->footcount = 0; st->sty_T = st->sty_L = st->sty_P = st->sty_Table = 1; st->chngs = NULL; st->chngsz = 0; rc = rndr(ob, &metaq, st, n); free(st->stys); free(st->chngs); lowdown_metaq_free(&metaq); hentryq_clear(&st->headers_used); return rc; } void * lowdown_odt_new(const struct lowdown_opts *opts) { struct odt *p; if ((p = calloc(1, sizeof(struct odt))) == NULL) return NULL; p->flags = opts == NULL ? 0 : opts->oflags; if (opts != NULL && opts->odt.sty != NULL && (p->sty = strdup(opts->odt.sty)) == NULL) { free(p); p = NULL; } return p; } void lowdown_odt_free(void *arg) { struct odt *p = arg; if (p != NULL) free(p->sty); free(p); }