diff --git a/text-crossrefs/Makefile b/text-crossrefs/Makefile
new file mode 100644
index 0000000..d9353c6
--- /dev/null
+++ b/text-crossrefs/Makefile
@@ -0,0 +1,23 @@
+.PHONY: test-all test-internal
+
+SHELL=/bin/bash
+return_statement_line_number := $(shell grep -nE '^return' text-crossrefs.lua | cut -d ':' -f 1)
+line_before_return := $(shell echo $$(($(return_statement_line_number) - 1)))
+
+test-all: test-internal test-context test-latex test-opendocument
+
+test-internal: text-crossrefs.lua
+ -rm --interactive=never test/tmp.lua
+ sed -n 1,$(line_before_return)p text-crossrefs.lua > test/tmp.lua
+ cat test/test-functions.lua >> test/tmp.lua
+ chmod -w test/tmp.lua
+ pandoc -L test/tmp.lua <<< ''
+ @echo -e '==========================\nAll internal tests passed.\n==========================\n'
+ rm --interactive=never test/tmp.lua
+
+test-%: text-crossrefs.lua sample.md
+ TESTED_FORMAT=$* pandoc -t native -L text-crossrefs.lua sample.md > test/tmp-$*.native
+ diff test/tmp-$*.native test/sample-$*.native
+ @echo -e '\n===============\ntest passed.\n===============\n'
+ rm test/tmp-$*.native
+
diff --git a/text-crossrefs/README.md b/text-crossrefs/README.md
new file mode 100644
index 0000000..87c9e2c
--- /dev/null
+++ b/text-crossrefs/README.md
@@ -0,0 +1,323 @@
+# text-crossrefs: cross-references to arbitrary portions of text in Pandoc
+
+This filter extends Pandoc's cross-referencing abilities
+with references to any portion of text
+by its page number, its note number (when applicable)
+or an arbitrary reference type (with ConTeXt or LaTeX output).
+It currently supports the following output formats:
+
+ * context
+ * docx
+ * latex
+ * odt
+ * opendocument
+
+## Format-specific preliminary notices
+
+### DOCX and ODT/Opendocument
+
+When opening for the first time a file produced by Pandoc with _text-crossrefs_,
+you should have to refresh the fields in order to get the correct values.
+In LibreOffice, press `F9`; in Word, a dialog box should appear when the file opens.
+
+### ConTeXt specifically
+
+Your template should include the following directive before `\starttext`
+(as in the default template for ConTeXt):
+
+```
+$for(header-includes)$
+$header-includes$
+$endfor$
+```
+
+### All TeX-based formats
+
+All references are wrapped in a macro named `\crossrefenum`.
+It has two optional arguments:
+the first one is the reference type,
+the second indicates whether the prefix (e.g. “p. ”) should be printed or not
+(can be set to `withprefix`, `noprefix`, `yes` or `no`).
+The default values for these arguments should match
+those of `tcrf-default-reftype` and `tcrf-default-prefixref`
+(resp. `page` and `yes`, i.e. `withprefix`).
+The mandatory argument of `\crossrefenum` is a group enclosing one or more groups.
+Each of them contain a reference (either a single reference or a range).
+
+Here are some valid invocations:
+
+ * `\crossrefenum[note][withprefix]{{lblone}{lbltwo}{lblthree}}`
+ * `\crossrefenum[page][noprefix]{{lblone}{lbltwo}{lblthree}}`
+ * `\crossrefenum[noprefix]{{lblone}{lbltwo}{lblthree}}` (the first argument defaults to `page`)
+ * `\crossrefenum{{lblone}{lbltwo}{lblthree}}` (the second argument defaults to `withprefix`)
+ * `\crossrefenum{{only-one}}` (even if the enumeration is limited to one item, it must be in a group)
+ * `\crossrefenum{{lblone to lbltwo}{lblthree}}` (the first reference points to a range)
+
+It is up to you to define `\crossrefenum` in your preamble.
+If your target format is LaTeX, it should be possible to define it as a wrapper
+for the `\zcref` macro provided by [the zref-clever package](https://ctan.org/pkg/zref-clever).
+Alternatively, you can use [my implementation](TODO),
+which currently supports ConTeXt and LaTeX.
+Here are some hints about the implementation of the `\crossrefenum` macro:
+
+ * [The `\crossrefenum` macro is supposed to output non only the numbers,
+ but also the prefixes and delimiters (e.g. “p. ” and “–”)]{#prefixes-tex};
+ * In ConTeXt, there is no way to retrieve the note number
+ from a `\reference` or a `\pagereference` contained in the note
+ as one would from a `\label` in LaTeX.
+ To work around this, footnotes are labelled automatically with `note:`
+ followed by the first identifier attached to a span in the note.
+ Contrary to the ConTeXt syntax, this label is placed _after_ the footnote content,
+ so the `\footnote` macro has to be redefined.
+ If your template includes the `header-includes` metadata variable like the default one does,
+ this redefinition will happen automatically.
+ Otherwise, you can copy-paste the following code in your preamble:
+
+ ``` {=tex}
+ \catcode`\@=11
+ \let\origfootnote\footnote
+ \def\footnote#1#2{
+ \def\crfnm@secondArg{#2}%
+ \ifx\crfnm@secondArg\crfnm@bracket
+ \def\crfnm@todo{\crfnm@footnote@withlabel{#1}#2} %
+ \else
+ \def\crfnm@todo{\origfootnote{#1}#2}%
+ \fi
+ \crfnm@todo
+ }
+ \def\crfnm@bracket{[}
+ \def\crfnm@footnote@withlabel#1[#2]{\origfootnote[#2]{#1}}
+ \catcode`\@=13
+
+ ```
+
+## Usage
+
+### Basics
+
+Mark the span of text you want to refer to later
+with an identifier composed of alphanumeric characters, periods, colons, underscores or hyphens:
+
+``` markdown
+Émile Gaboriau published [_L'Affaire Lerouge_ in
+1866]{#publication}.[^1]
+
+[^1]: It is a very [fine piece of literature]{#my-evaluation}.
+
+[It was very popular.]{#reception}
+```
+
+To refer to it, write another span with class `tcrf` containing the target's identifier.
+If the span you are referring to is part of a footnote,
+you can refer to it either by page or note number
+according to the value of the `reftype` attribute (defaults to `page`).
+For instance, this:
+
+``` markdown
+See [publication]{.tcrf} for the publication date. I gave my
+opinion in [my-evaluation]{.tcrf reftype=note}, [my-evaluation]{.tcrf}.
+```
+
+will render in ConTeXt or LaTeX output:
+
+``` tex
+See \crossrefenum{{publication}} for the publication date. I expressed
+my thoughts about it in \crossrefenum[note]{{my-evaluation}},
+\crossrefenum{{my-evaluation}}.
+```
+
+If you want to give a reference by note _and_ page number like in the example above,
+you can also use the following shorthand:
+
+```md
+[my-evaluation]{.tcrf reftype=pagenote}
+```
+
+In fact, you can use any identifier, including those automatically set by Pandoc.
+
+To suppress the prefixes (e.g. “p. ”), you can set the `prefixref` attribute to `no` (defaults to `yes`).
+It can be useful, for instance, for small manually formatted indexes[^comma-syntax]:
+
+``` markdown
+Gaboriau: [publication, my-evaluation, reception]{.tcrf prefixref=no}
+```
+
+[^comma-syntax]: About the comma-delimited syntax used in this example, see [the section on enumerations below](#enums).
+
+### Controlling where the label is placed
+
+By default, labels are placed at the beginning of the spans.
+You can change this via the attribute `refanchor`, which can be set to:
+
+ * `beg` (default);
+ * `end`;
+ * `both`.
+
+When the value is `both`, _text-crossrefs_ creates two labels
+by suffixing `-beg` and `-end` to the identifier.
+A typical use case is:
+
+```
+[A portion of text that may cross a page break.]{#mylbl refanchor=both}
+
+See [mylbl-beg>mylbl-end]{.tcrf}.
+```
+
+### Page ranges
+
+You can refer to a page range like this:
+
+``` markdown
+If you want to know more about _L'Affaire Lerouge_, see [publication>reception]{.tcrf}.
+```
+
+The separator (here `>`) can be set to any string of characters
+other than alphanumeric, period, colon, underscore, hyphen and space
+(see the [customization options](#common-opts)).
+
+In LaTeX and ConTeXt output, the above-mentionned `\crossrefenum` macro
+should be defined so that the range is printed as a simple page reference if the page numbers are identical.
+The syntax of a range is[^customize-range-tex]:
+
+``` tex
+\crossrefenum{{publication to reception}}
+```
+
+[^customize-range-tex]: Note that [it can be customized](#tex-options).
+
+In DOCX and ODT/Opendocument output,
+a range will be printed as a range even if the numbers are identical.
+
+### Enumerations {#enums}
+
+You can enumerate several references as a comma-delimited list, for instance:
+
+``` markdown
+[ref-one, ref-two>ref-three, ref-four]{.tcrf}
+```
+
+In DOCX and ODT/Opendocument output, all these references will be printed,
+potentially resulting in unnecessary repetitions.
+In TeX-based output formats, they will be wrapped in `\crossrefenum` like this
+and should be collapsed by the macro when it is desirable:
+
+``` tex
+\crossrefenum{{ref-one}{ref-two to ref-three}{ref-four}}
+```
+
+## Customization
+
+### Common options {#common-opts}
+
+The following metadata fields can be set as strings:
+
+ * `tcrf-references-enum-separator`:
+ * the string between two references in an enumeration in a reference span;
+ * defaults to a comma followed by a space.
+ * `tcrf-references-range-separator`:
+ * the string used to separate two references in a reference span;
+ * defaults to `>`.
+ * `tcrf-only-explicit-labels`:
+ * set it to `true` if you want _crossrefenum_ to refer to spans with class `label` only;
+ * defaults to `false`.
+ * `tcrf-default-prefixref`:
+ * default value for the `prefixref` attribute;
+ * defaults to `yes`.
+ * `tcrf-default-reftype`:
+ * default value for the `reftype` attribute;
+ * defaults to `page`.
+
+### Options specific to DOCX and ODT/Opendocument
+
+Here are some metadata fields for the `docx`, `odt` and `opendocument` formats only
+(see [above](#prefixes-tex) why they are ignored in ConTeXt and LaTeX output):
+
+ * `tcrf-page-prefix`:
+ * “page” prefix;
+ * defaults to `p. `.
+ * `tcrf-pages-prefix`:
+ * “pages” prefix;
+ * defaults to `pp. `.
+ * `tcrf-note-prefix`:
+ * “note” prefix;
+ * defaults to `n. `.
+ * `tcrf-notes-prefix`:
+ * “notes” prefix;
+ * defaults to `nn. `.
+ * `tcrf-pagenote-separator`:
+ * the separator between the enumerated references in the output file
+ when `reftype` is set to `pagenote`;
+ * defaults to a comma followed by a space.
+ * `tcrf-pagenote-at-end`:
+ * the string printed at the end of a `pagenote` reference in the output file;
+ * defaults to the empty string, but it can be used to achieve something like *n. 3 (p. 5)*
+ (see the sample file).
+ * `tcrf-pagenote-factorize-first-prefix-in-enum`:
+ * defines if the prefixes of the type printed first in a reference to page and note
+ should be repeated (like in “p. 6, n. 1 and p. 9, n. 3”)
+ or set globally at the beginning of the enumeration (like in “pp. 6, n. 1 and 9, n. 3”);
+ * defaults to `no`, can be set to `yes`.
+ * `tcrf-pagenote-first-type`:
+ * the type of the reference number that is printed first in references to page and note;
+ * defaults to `page`, can be set to `note`.
+ * `tcrf-range-separator`:
+ * the string separating the page numbers in a range;
+ * defaults to `–`.
+ * `tcrf-references-enum-separator`:
+ * the string separating the elements of an enumeration in a reference span;
+ * defaults to a comma followed by a space.
+ * `tcrf-multiple-delimiter`:
+ * the string between two elements (but the two last ones) in an enumeration;
+ * defaults to a comma followed by a space.
+ * `tcrf-multiple-before-last`:
+ * the string inserted between the two last elements in an enumeration;
+ * defaults to `and` surrounded with spaces.
+
+### Options specific to the formats based on TeX {#tex-options}
+
+Since TeX is extensible, you may wish to support types
+other than `page`, `note` and `pagenote` for ConTeXt and LaTeX output.
+`tcrf-additional-types` can be provided with a list of supplementary accepted types, e.g.:
+
+``` yaml
+tcrf-additional-types:
+- line
+- figure
+```
+
+Once declared, these types can be used as the value of `reftype`
+in the same way as `page`, `note` and `pagenote`.
+
+In addition, the following metadata field can be used to control the rendering of ranges of labels in `\crossrefenum`:
+
+ * `tcrf-range-delim-crossrefenum`:
+ * the delimiter between the labels of a range in the TeX output file;
+ * defaults to `to` surrounded with spaces.
+
+## Compatibility with other filters
+
+_text-crossrefs_ must be run after all filters that may create, delete or move footnotes, such as citeproc.
+
+In a citation inside square brackets, the span bearing an identifier should not include
+a citation key, a locator or a `;` delimiter.
+When it follows immediatly the locator,
+you should protect the locator with curly brackets.
+For example, this should work:
+
+ ``` markdown
+ [@Jones1973, p. 5-70; @Doe2004[]{#jones-doe}]
+
+ [@Jones1973, p. 5-70; [it was elaborated upon]{#further-elaboration} by @Doe2004]
+
+ [@Jones1973, {p. 5-70}[]{#ref-to-jones}; @Doe2004]
+ ```
+
+not that:
+
+ ``` markdown
+ [[@Jones1973, p. 5-70]{#ref-to-jones}; @Doe2004]
+
+ [[@Jones1973, p. 5-70; @Doe2004]{#jones-doe}]
+
+ [@Jones1973, p. 5-70[]{#ref-to-jones}; @Doe2004]
+ ```
diff --git a/text-crossrefs/sample.md b/text-crossrefs/sample.md
new file mode 100644
index 0000000..a4a0906
--- /dev/null
+++ b/text-crossrefs/sample.md
@@ -0,0 +1,41 @@
+---
+tcrf-pagenote-separator: '\ ('
+tcrf-pagenote-at-end: ')'
+header-includes: |
+ \input{crossrefenum}
+---
+
+(About the notes, see [toc-notes-begin>toc-notes-end]{.tcrf}.)
+
+Émile Gaboriau published [_L'Affaire Lerouge_ in
+1866]{#publication}.[^1]
+
+[^1]: It is a very [fine piece of literature]{#my-evaluation}.
+
+[It was very popular.]{#reception}
+
+See [publication]{.tcrf} for the publication date. I expressed
+my thoughts about it in [my-evaluation]{.tcrf reftype=pagenote}.
+
+If you want to know more about _L'Affaire Lerouge_, see [publication>reception]{.tcrf}.
+
+Here are some precisions.[^2]
+
+[^2]: [Whatever format]{#format} you choose, you can [refer to a note]{#refer-to-note} by the identifier of [any of its spans. You can even [nest spans]{#nested-spans}!]{#which-identifier}
+
+[I want to refer to a note]{#toc-notes-begin}:
+
+ * How can I refer to a note by its number? → See [refer-to-note]{.tcrf}.
+ * What formats are supported? → See [format]{.tcrf}.
+ * What if the note contains multiple spans with identifiers? → See [which-identifier]{.tcrf}.
+ * What happens if a span in contained in a span? → See [nested-spans]{.tcrf}.
+ * What are the notes? → [my-evaluation, format, refer-to-note]{.tcrf reftype=note}
+ * Where are the notes? → [my-evaluation, format]{.tcrf}
+
+[]{#toc-notes-end}
+
+[A portion of text that may cross a page break.]{#doubledlbl refanchor=both}
+
+[And this one is labelized at the end.]{#lblatend refanchor=end}
+
+See [doubledlbl-beg>doubledlbl-end]{.tcrf}.
diff --git a/text-crossrefs/test/sample-context.native b/text-crossrefs/test/sample-context.native
new file mode 100644
index 0000000..30f7c06
--- /dev/null
+++ b/text-crossrefs/test/sample-context.native
@@ -0,0 +1,456 @@
+[ Para
+ [ Str "(About"
+ , Space
+ , Str "the"
+ , Space
+ , Str "notes,"
+ , Space
+ , Str "see"
+ , Space
+ , Span
+ ( "" , [ "tcrf" ] , [] )
+ [ RawInline
+ (Format "context")
+ "\\crossrefenum{{toc-notes-begin to toc-notes-end}}"
+ ]
+ , Str ".)"
+ ]
+, Para
+ [ Str "\201mile"
+ , Space
+ , Str "Gaboriau"
+ , Space
+ , Str "published"
+ , Space
+ , Str ""
+ , Span
+ ( "publication" , [] , [] )
+ [ Emph [ Str "L\8217Affaire" , Space , Str "Lerouge" ]
+ , Space
+ , Str "in"
+ , SoftBreak
+ , Str "1866"
+ ]
+ , Str "."
+ , RawInline
+ (Format "context") "\\withfirstopt[note:my-evaluation]"
+ , Note
+ [ Para
+ [ Str "It"
+ , Space
+ , Str "is"
+ , Space
+ , Str "a"
+ , Space
+ , Str "very"
+ , Space
+ , Str ""
+ , Span
+ ( "my-evaluation" , [] , [] )
+ [ Str "fine"
+ , Space
+ , Str "piece"
+ , Space
+ , Str "of"
+ , Space
+ , Str "literature"
+ ]
+ , Str "."
+ ]
+ ]
+ , RawInline (Format "context") ""
+ ]
+, Para
+ [ Str ""
+ , Span
+ ( "reception" , [] , [] )
+ [ Str "It"
+ , Space
+ , Str "was"
+ , Space
+ , Str "very"
+ , Space
+ , Str "popular."
+ ]
+ ]
+, Para
+ [ Str "See"
+ , Space
+ , Span
+ ( "" , [ "tcrf" ] , [] )
+ [ RawInline
+ (Format "context") "\\crossrefenum{{publication}}"
+ ]
+ , Space
+ , Str "for"
+ , Space
+ , Str "the"
+ , Space
+ , Str "publication"
+ , Space
+ , Str "date."
+ , Space
+ , Str "I"
+ , Space
+ , Str "expressed"
+ , SoftBreak
+ , Str "my"
+ , Space
+ , Str "thoughts"
+ , Space
+ , Str "about"
+ , Space
+ , Str "it"
+ , Space
+ , Str "in"
+ , Space
+ , Span
+ ( "" , [ "tcrf" ] , [ ( "reftype" , "pagenote" ) ] )
+ [ RawInline
+ (Format "context")
+ "\\crossrefenum[pagenote]{{my-evaluation}}"
+ ]
+ , Str "."
+ ]
+, Para
+ [ Str "If"
+ , Space
+ , Str "you"
+ , Space
+ , Str "want"
+ , Space
+ , Str "to"
+ , Space
+ , Str "know"
+ , Space
+ , Str "more"
+ , Space
+ , Str "about"
+ , Space
+ , Emph [ Str "L\8217Affaire" , Space , Str "Lerouge" ]
+ , Str ","
+ , Space
+ , Str "see"
+ , Space
+ , Span
+ ( "" , [ "tcrf" ] , [] )
+ [ RawInline
+ (Format "context")
+ "\\crossrefenum{{publication to reception}}"
+ ]
+ , Str "."
+ ]
+, Para
+ [ Str "Here"
+ , Space
+ , Str "are"
+ , Space
+ , Str "some"
+ , Space
+ , Str "precisions."
+ , RawInline (Format "context") "\\withfirstopt[note:format]"
+ , Note
+ [ Para
+ [ Str ""
+ , Span
+ ( "format" , [] , [] )
+ [ Str "Whatever" , Space , Str "format" ]
+ , Space
+ , Str "you"
+ , Space
+ , Str "choose,"
+ , Space
+ , Str "you"
+ , Space
+ , Str "can"
+ , Space
+ , Str ""
+ , Span
+ ( "refer-to-note" , [] , [] )
+ [ Str "refer"
+ , Space
+ , Str "to"
+ , Space
+ , Str "a"
+ , Space
+ , Str "note"
+ ]
+ , Space
+ , Str "by"
+ , Space
+ , Str "the"
+ , Space
+ , Str "identifier"
+ , Space
+ , Str "of"
+ , Space
+ , Str ""
+ , Span
+ ( "which-identifier" , [] , [] )
+ [ Str "any"
+ , Space
+ , Str "of"
+ , Space
+ , Str "its"
+ , Space
+ , Str "spans."
+ , Space
+ , Str "You"
+ , Space
+ , Str "can"
+ , Space
+ , Str "even"
+ , Space
+ , Str ""
+ , Span
+ ( "nested-spans" , [] , [] )
+ [ Str "nest" , Space , Str "spans" ]
+ , Str "!"
+ ]
+ ]
+ ]
+ , RawInline (Format "context") ""
+ ]
+, Para
+ [ Str ""
+ , Span
+ ( "toc-notes-begin" , [] , [] )
+ [ Str "I"
+ , Space
+ , Str "want"
+ , Space
+ , Str "to"
+ , Space
+ , Str "refer"
+ , Space
+ , Str "to"
+ , Space
+ , Str "a"
+ , Space
+ , Str "note"
+ ]
+ , Str ":"
+ ]
+, BulletList
+ [ [ Plain
+ [ Str "How"
+ , Space
+ , Str "can"
+ , Space
+ , Str "I"
+ , Space
+ , Str "refer"
+ , Space
+ , Str "to"
+ , Space
+ , Str "a"
+ , Space
+ , Str "note"
+ , Space
+ , Str "by"
+ , Space
+ , Str "its"
+ , Space
+ , Str "number?"
+ , Space
+ , Str "\8594"
+ , Space
+ , Str "See"
+ , Space
+ , Span
+ ( "" , [ "tcrf" ] , [] )
+ [ RawInline
+ (Format "context") "\\crossrefenum{{refer-to-note}}"
+ ]
+ , Str "."
+ ]
+ ]
+ , [ Plain
+ [ Str "What"
+ , Space
+ , Str "formats"
+ , Space
+ , Str "are"
+ , Space
+ , Str "supported?"
+ , Space
+ , Str "\8594"
+ , Space
+ , Str "See"
+ , Space
+ , Span
+ ( "" , [ "tcrf" ] , [] )
+ [ RawInline (Format "context") "\\crossrefenum{{format}}"
+ ]
+ , Str "."
+ ]
+ ]
+ , [ Plain
+ [ Str "What"
+ , Space
+ , Str "if"
+ , Space
+ , Str "the"
+ , Space
+ , Str "note"
+ , Space
+ , Str "contains"
+ , Space
+ , Str "multiple"
+ , Space
+ , Str "spans"
+ , Space
+ , Str "with"
+ , Space
+ , Str "identifiers?"
+ , Space
+ , Str "\8594"
+ , Space
+ , Str "See"
+ , Space
+ , Span
+ ( "" , [ "tcrf" ] , [] )
+ [ RawInline
+ (Format "context")
+ "\\crossrefenum{{which-identifier}}"
+ ]
+ , Str "."
+ ]
+ ]
+ , [ Plain
+ [ Str "What"
+ , Space
+ , Str "happens"
+ , Space
+ , Str "if"
+ , Space
+ , Str "a"
+ , Space
+ , Str "span"
+ , Space
+ , Str "in"
+ , Space
+ , Str "contained"
+ , Space
+ , Str "in"
+ , Space
+ , Str "a"
+ , Space
+ , Str "span?"
+ , Space
+ , Str "\8594"
+ , Space
+ , Str "See"
+ , Space
+ , Span
+ ( "" , [ "tcrf" ] , [] )
+ [ RawInline
+ (Format "context") "\\crossrefenum{{nested-spans}}"
+ ]
+ , Str "."
+ ]
+ ]
+ , [ Plain
+ [ Str "What"
+ , Space
+ , Str "are"
+ , Space
+ , Str "the"
+ , Space
+ , Str "notes?"
+ , Space
+ , Str "\8594"
+ , Space
+ , Span
+ ( "" , [ "tcrf" ] , [ ( "reftype" , "note" ) ] )
+ [ RawInline
+ (Format "context")
+ "\\crossrefenum[note]{{my-evaluation}{format}{refer-to-note}}"
+ ]
+ ]
+ ]
+ , [ Plain
+ [ Str "Where"
+ , Space
+ , Str "are"
+ , Space
+ , Str "the"
+ , Space
+ , Str "notes?"
+ , Space
+ , Str "\8594"
+ , Space
+ , Span
+ ( "" , [ "tcrf" ] , [] )
+ [ RawInline
+ (Format "context")
+ "\\crossrefenum{{my-evaluation}{format}}"
+ ]
+ ]
+ ]
+ ]
+, Para [ Str "" , Span ( "toc-notes-end" , [] , [] ) [] ]
+, Para
+ [ Str ""
+ , Span
+ ( "doubledlbl" , [] , [ ( "refanchor" , "both" ) ] )
+ [ Str ""
+ , Span ( "doubledlbl-beg" , [] , [] ) []
+ , Str "A"
+ , Space
+ , Str "portion"
+ , Space
+ , Str "of"
+ , Space
+ , Str "text"
+ , Space
+ , Str "that"
+ , Space
+ , Str "may"
+ , Space
+ , Str "cross"
+ , Space
+ , Str "a"
+ , Space
+ , Str "page"
+ , Space
+ , Str "break."
+ , Str ""
+ , Span ( "doubledlbl-end" , [] , [] ) []
+ ]
+ ]
+, Para
+ [ Str ""
+ , Span
+ ( "lblatend" , [] , [ ( "refanchor" , "end" ) ] )
+ [ Str "And"
+ , Space
+ , Str "this"
+ , Space
+ , Str "one"
+ , Space
+ , Str "is"
+ , Space
+ , Str "labelized"
+ , Space
+ , Str "at"
+ , Space
+ , Str "the"
+ , Space
+ , Str "end."
+ , Str ""
+ , Span ( "lblatend" , [] , [] ) []
+ ]
+ ]
+, Para
+ [ Str "See"
+ , Space
+ , Span
+ ( "" , [ "tcrf" ] , [] )
+ [ RawInline
+ (Format "context")
+ "\\crossrefenum{{doubledlbl-beg to doubledlbl-end}}"
+ ]
+ , Str "."
+ ]
+]
diff --git a/text-crossrefs/test/sample-latex.native b/text-crossrefs/test/sample-latex.native
new file mode 100644
index 0000000..7cfcb88
--- /dev/null
+++ b/text-crossrefs/test/sample-latex.native
@@ -0,0 +1,454 @@
+[ Para
+ [ Str "(About"
+ , Space
+ , Str "the"
+ , Space
+ , Str "notes,"
+ , Space
+ , Str "see"
+ , Space
+ , Span
+ ( "" , [ "tcrf" ] , [] )
+ [ RawInline
+ (Format "latex")
+ "\\crossrefenum{{toc-notes-begin to toc-notes-end}}"
+ ]
+ , Str ".)"
+ ]
+, Para
+ [ Str "\201mile"
+ , Space
+ , Str "Gaboriau"
+ , Space
+ , Str "published"
+ , Space
+ , RawInline (Format "latex") "\\label{publication}"
+ , Span
+ ( "publication" , [] , [] )
+ [ Emph [ Str "L\8217Affaire" , Space , Str "Lerouge" ]
+ , Space
+ , Str "in"
+ , SoftBreak
+ , Str "1866"
+ ]
+ , Str "."
+ , RawInline (Format "latex") ""
+ , Note
+ [ Para
+ [ Str "It"
+ , Space
+ , Str "is"
+ , Space
+ , Str "a"
+ , Space
+ , Str "very"
+ , Space
+ , RawInline (Format "latex") "\\label{my-evaluation}"
+ , Span
+ ( "my-evaluation" , [] , [] )
+ [ Str "fine"
+ , Space
+ , Str "piece"
+ , Space
+ , Str "of"
+ , Space
+ , Str "literature"
+ ]
+ , Str "."
+ ]
+ ]
+ , RawInline (Format "latex") ""
+ ]
+, Para
+ [ RawInline (Format "latex") "\\label{reception}"
+ , Span
+ ( "reception" , [] , [] )
+ [ Str "It"
+ , Space
+ , Str "was"
+ , Space
+ , Str "very"
+ , Space
+ , Str "popular."
+ ]
+ ]
+, Para
+ [ Str "See"
+ , Space
+ , Span
+ ( "" , [ "tcrf" ] , [] )
+ [ RawInline (Format "latex") "\\crossrefenum{{publication}}"
+ ]
+ , Space
+ , Str "for"
+ , Space
+ , Str "the"
+ , Space
+ , Str "publication"
+ , Space
+ , Str "date."
+ , Space
+ , Str "I"
+ , Space
+ , Str "expressed"
+ , SoftBreak
+ , Str "my"
+ , Space
+ , Str "thoughts"
+ , Space
+ , Str "about"
+ , Space
+ , Str "it"
+ , Space
+ , Str "in"
+ , Space
+ , Span
+ ( "" , [ "tcrf" ] , [ ( "reftype" , "pagenote" ) ] )
+ [ RawInline
+ (Format "latex") "\\crossrefenum[pagenote]{{my-evaluation}}"
+ ]
+ , Str "."
+ ]
+, Para
+ [ Str "If"
+ , Space
+ , Str "you"
+ , Space
+ , Str "want"
+ , Space
+ , Str "to"
+ , Space
+ , Str "know"
+ , Space
+ , Str "more"
+ , Space
+ , Str "about"
+ , Space
+ , Emph [ Str "L\8217Affaire" , Space , Str "Lerouge" ]
+ , Str ","
+ , Space
+ , Str "see"
+ , Space
+ , Span
+ ( "" , [ "tcrf" ] , [] )
+ [ RawInline
+ (Format "latex")
+ "\\crossrefenum{{publication to reception}}"
+ ]
+ , Str "."
+ ]
+, Para
+ [ Str "Here"
+ , Space
+ , Str "are"
+ , Space
+ , Str "some"
+ , Space
+ , Str "precisions."
+ , RawInline (Format "latex") ""
+ , Note
+ [ Para
+ [ RawInline (Format "latex") "\\label{format}"
+ , Span
+ ( "format" , [] , [] )
+ [ Str "Whatever" , Space , Str "format" ]
+ , Space
+ , Str "you"
+ , Space
+ , Str "choose,"
+ , Space
+ , Str "you"
+ , Space
+ , Str "can"
+ , Space
+ , RawInline (Format "latex") "\\label{refer-to-note}"
+ , Span
+ ( "refer-to-note" , [] , [] )
+ [ Str "refer"
+ , Space
+ , Str "to"
+ , Space
+ , Str "a"
+ , Space
+ , Str "note"
+ ]
+ , Space
+ , Str "by"
+ , Space
+ , Str "the"
+ , Space
+ , Str "identifier"
+ , Space
+ , Str "of"
+ , Space
+ , RawInline (Format "latex") "\\label{which-identifier}"
+ , Span
+ ( "which-identifier" , [] , [] )
+ [ Str "any"
+ , Space
+ , Str "of"
+ , Space
+ , Str "its"
+ , Space
+ , Str "spans."
+ , Space
+ , Str "You"
+ , Space
+ , Str "can"
+ , Space
+ , Str "even"
+ , Space
+ , RawInline (Format "latex") "\\label{nested-spans}"
+ , Span
+ ( "nested-spans" , [] , [] )
+ [ Str "nest" , Space , Str "spans" ]
+ , Str "!"
+ ]
+ ]
+ ]
+ , RawInline (Format "latex") ""
+ ]
+, Para
+ [ RawInline (Format "latex") "\\label{toc-notes-begin}"
+ , Span
+ ( "toc-notes-begin" , [] , [] )
+ [ Str "I"
+ , Space
+ , Str "want"
+ , Space
+ , Str "to"
+ , Space
+ , Str "refer"
+ , Space
+ , Str "to"
+ , Space
+ , Str "a"
+ , Space
+ , Str "note"
+ ]
+ , Str ":"
+ ]
+, BulletList
+ [ [ Plain
+ [ Str "How"
+ , Space
+ , Str "can"
+ , Space
+ , Str "I"
+ , Space
+ , Str "refer"
+ , Space
+ , Str "to"
+ , Space
+ , Str "a"
+ , Space
+ , Str "note"
+ , Space
+ , Str "by"
+ , Space
+ , Str "its"
+ , Space
+ , Str "number?"
+ , Space
+ , Str "\8594"
+ , Space
+ , Str "See"
+ , Space
+ , Span
+ ( "" , [ "tcrf" ] , [] )
+ [ RawInline
+ (Format "latex") "\\crossrefenum{{refer-to-note}}"
+ ]
+ , Str "."
+ ]
+ ]
+ , [ Plain
+ [ Str "What"
+ , Space
+ , Str "formats"
+ , Space
+ , Str "are"
+ , Space
+ , Str "supported?"
+ , Space
+ , Str "\8594"
+ , Space
+ , Str "See"
+ , Space
+ , Span
+ ( "" , [ "tcrf" ] , [] )
+ [ RawInline (Format "latex") "\\crossrefenum{{format}}" ]
+ , Str "."
+ ]
+ ]
+ , [ Plain
+ [ Str "What"
+ , Space
+ , Str "if"
+ , Space
+ , Str "the"
+ , Space
+ , Str "note"
+ , Space
+ , Str "contains"
+ , Space
+ , Str "multiple"
+ , Space
+ , Str "spans"
+ , Space
+ , Str "with"
+ , Space
+ , Str "identifiers?"
+ , Space
+ , Str "\8594"
+ , Space
+ , Str "See"
+ , Space
+ , Span
+ ( "" , [ "tcrf" ] , [] )
+ [ RawInline
+ (Format "latex") "\\crossrefenum{{which-identifier}}"
+ ]
+ , Str "."
+ ]
+ ]
+ , [ Plain
+ [ Str "What"
+ , Space
+ , Str "happens"
+ , Space
+ , Str "if"
+ , Space
+ , Str "a"
+ , Space
+ , Str "span"
+ , Space
+ , Str "in"
+ , Space
+ , Str "contained"
+ , Space
+ , Str "in"
+ , Space
+ , Str "a"
+ , Space
+ , Str "span?"
+ , Space
+ , Str "\8594"
+ , Space
+ , Str "See"
+ , Space
+ , Span
+ ( "" , [ "tcrf" ] , [] )
+ [ RawInline
+ (Format "latex") "\\crossrefenum{{nested-spans}}"
+ ]
+ , Str "."
+ ]
+ ]
+ , [ Plain
+ [ Str "What"
+ , Space
+ , Str "are"
+ , Space
+ , Str "the"
+ , Space
+ , Str "notes?"
+ , Space
+ , Str "\8594"
+ , Space
+ , Span
+ ( "" , [ "tcrf" ] , [ ( "reftype" , "note" ) ] )
+ [ RawInline
+ (Format "latex")
+ "\\crossrefenum[note]{{my-evaluation}{format}{refer-to-note}}"
+ ]
+ ]
+ ]
+ , [ Plain
+ [ Str "Where"
+ , Space
+ , Str "are"
+ , Space
+ , Str "the"
+ , Space
+ , Str "notes?"
+ , Space
+ , Str "\8594"
+ , Space
+ , Span
+ ( "" , [ "tcrf" ] , [] )
+ [ RawInline
+ (Format "latex")
+ "\\crossrefenum{{my-evaluation}{format}}"
+ ]
+ ]
+ ]
+ ]
+, Para
+ [ RawInline (Format "latex") "\\label{toc-notes-end}"
+ , Span ( "toc-notes-end" , [] , [] ) []
+ ]
+, Para
+ [ RawInline (Format "latex") "\\label{doubledlbl}"
+ , Span
+ ( "doubledlbl" , [] , [ ( "refanchor" , "both" ) ] )
+ [ RawInline (Format "latex") "\\label{doubledlbl-beg}"
+ , Span ( "doubledlbl-beg" , [] , [] ) []
+ , Str "A"
+ , Space
+ , Str "portion"
+ , Space
+ , Str "of"
+ , Space
+ , Str "text"
+ , Space
+ , Str "that"
+ , Space
+ , Str "may"
+ , Space
+ , Str "cross"
+ , Space
+ , Str "a"
+ , Space
+ , Str "page"
+ , Space
+ , Str "break."
+ , RawInline (Format "latex") "\\label{doubledlbl-end}"
+ , Span ( "doubledlbl-end" , [] , [] ) []
+ ]
+ ]
+, Para
+ [ RawInline (Format "latex") "\\label{lblatend}"
+ , Span
+ ( "lblatend" , [] , [ ( "refanchor" , "end" ) ] )
+ [ Str "And"
+ , Space
+ , Str "this"
+ , Space
+ , Str "one"
+ , Space
+ , Str "is"
+ , Space
+ , Str "labelized"
+ , Space
+ , Str "at"
+ , Space
+ , Str "the"
+ , Space
+ , Str "end."
+ , RawInline (Format "latex") "\\label{lblatend}"
+ , Span ( "lblatend" , [] , [] ) []
+ ]
+ ]
+, Para
+ [ Str "See"
+ , Space
+ , Span
+ ( "" , [ "tcrf" ] , [] )
+ [ RawInline
+ (Format "latex")
+ "\\crossrefenum{{doubledlbl-beg to doubledlbl-end}}"
+ ]
+ , Str "."
+ ]
+]
diff --git a/text-crossrefs/test/sample-opendocument.native b/text-crossrefs/test/sample-opendocument.native
new file mode 100644
index 0000000..fbd1f37
--- /dev/null
+++ b/text-crossrefs/test/sample-opendocument.native
@@ -0,0 +1,460 @@
+[ Para
+ [ Str "(About"
+ , Space
+ , Str "the"
+ , Space
+ , Str "notes,"
+ , Space
+ , Str "see"
+ , Space
+ , Span
+ ( "" , [ "tcrf" ] , [] )
+ [ RawInline
+ (Format "opendocument")
+ "pp.\160000\8211000"
+ ]
+ , Str ".)"
+ ]
+, Para
+ [ Str "\201mile"
+ , Space
+ , Str "Gaboriau"
+ , Space
+ , Str "published"
+ , Space
+ , Str ""
+ , Span
+ ( "publication" , [] , [] )
+ [ Emph [ Str "L\8217Affaire" , Space , Str "Lerouge" ]
+ , Space
+ , Str "in"
+ , SoftBreak
+ , Str "1866"
+ ]
+ , Str "."
+ , RawInline (Format "opendocument") ""
+ , Note
+ [ Para
+ [ Str "It"
+ , Space
+ , Str "is"
+ , Space
+ , Str "a"
+ , Space
+ , Str "very"
+ , Space
+ , Str ""
+ , Span
+ ( "my-evaluation" , [] , [] )
+ [ Str "fine"
+ , Space
+ , Str "piece"
+ , Space
+ , Str "of"
+ , Space
+ , Str "literature"
+ ]
+ , Str "."
+ ]
+ ]
+ , RawInline (Format "opendocument") ""
+ ]
+, Para
+ [ Str ""
+ , Span
+ ( "reception" , [] , [] )
+ [ Str "It"
+ , Space
+ , Str "was"
+ , Space
+ , Str "very"
+ , Space
+ , Str "popular."
+ ]
+ ]
+, Para
+ [ Str "See"
+ , Space
+ , Span
+ ( "" , [ "tcrf" ] , [] )
+ [ RawInline
+ (Format "opendocument")
+ "p.\160000"
+ ]
+ , Space
+ , Str "for"
+ , Space
+ , Str "the"
+ , Space
+ , Str "publication"
+ , Space
+ , Str "date."
+ , Space
+ , Str "I"
+ , Space
+ , Str "expressed"
+ , SoftBreak
+ , Str "my"
+ , Space
+ , Str "thoughts"
+ , Space
+ , Str "about"
+ , Space
+ , Str "it"
+ , Space
+ , Str "in"
+ , Space
+ , Span
+ ( "" , [ "tcrf" ] , [ ( "reftype" , "pagenote" ) ] )
+ [ RawInline
+ (Format "opendocument")
+ "p.\160000\160(n.\160000)"
+ ]
+ , Str "."
+ ]
+, Para
+ [ Str "If"
+ , Space
+ , Str "you"
+ , Space
+ , Str "want"
+ , Space
+ , Str "to"
+ , Space
+ , Str "know"
+ , Space
+ , Str "more"
+ , Space
+ , Str "about"
+ , Space
+ , Emph [ Str "L\8217Affaire" , Space , Str "Lerouge" ]
+ , Str ","
+ , Space
+ , Str "see"
+ , Space
+ , Span
+ ( "" , [ "tcrf" ] , [] )
+ [ RawInline
+ (Format "opendocument")
+ "pp.\160000\8211000"
+ ]
+ , Str "."
+ ]
+, Para
+ [ Str "Here"
+ , Space
+ , Str "are"
+ , Space
+ , Str "some"
+ , Space
+ , Str "precisions."
+ , RawInline (Format "opendocument") ""
+ , Note
+ [ Para
+ [ Str ""
+ , Span
+ ( "format" , [] , [] )
+ [ Str "Whatever" , Space , Str "format" ]
+ , Space
+ , Str "you"
+ , Space
+ , Str "choose,"
+ , Space
+ , Str "you"
+ , Space
+ , Str "can"
+ , Space
+ , Str ""
+ , Span
+ ( "refer-to-note" , [] , [] )
+ [ Str "refer"
+ , Space
+ , Str "to"
+ , Space
+ , Str "a"
+ , Space
+ , Str "note"
+ ]
+ , Space
+ , Str "by"
+ , Space
+ , Str "the"
+ , Space
+ , Str "identifier"
+ , Space
+ , Str "of"
+ , Space
+ , Str ""
+ , Span
+ ( "which-identifier" , [] , [] )
+ [ Str "any"
+ , Space
+ , Str "of"
+ , Space
+ , Str "its"
+ , Space
+ , Str "spans."
+ , Space
+ , Str "You"
+ , Space
+ , Str "can"
+ , Space
+ , Str "even"
+ , Space
+ , Str ""
+ , Span
+ ( "nested-spans" , [] , [] )
+ [ Str "nest" , Space , Str "spans" ]
+ , Str "!"
+ ]
+ ]
+ ]
+ , RawInline (Format "opendocument") ""
+ ]
+, Para
+ [ Str ""
+ , Span
+ ( "toc-notes-begin" , [] , [] )
+ [ Str "I"
+ , Space
+ , Str "want"
+ , Space
+ , Str "to"
+ , Space
+ , Str "refer"
+ , Space
+ , Str "to"
+ , Space
+ , Str "a"
+ , Space
+ , Str "note"
+ ]
+ , Str ":"
+ ]
+, BulletList
+ [ [ Plain
+ [ Str "How"
+ , Space
+ , Str "can"
+ , Space
+ , Str "I"
+ , Space
+ , Str "refer"
+ , Space
+ , Str "to"
+ , Space
+ , Str "a"
+ , Space
+ , Str "note"
+ , Space
+ , Str "by"
+ , Space
+ , Str "its"
+ , Space
+ , Str "number?"
+ , Space
+ , Str "\8594"
+ , Space
+ , Str "See"
+ , Space
+ , Span
+ ( "" , [ "tcrf" ] , [] )
+ [ RawInline
+ (Format "opendocument")
+ "p.\160000"
+ ]
+ , Str "."
+ ]
+ ]
+ , [ Plain
+ [ Str "What"
+ , Space
+ , Str "formats"
+ , Space
+ , Str "are"
+ , Space
+ , Str "supported?"
+ , Space
+ , Str "\8594"
+ , Space
+ , Str "See"
+ , Space
+ , Span
+ ( "" , [ "tcrf" ] , [] )
+ [ RawInline
+ (Format "opendocument")
+ "p.\160000"
+ ]
+ , Str "."
+ ]
+ ]
+ , [ Plain
+ [ Str "What"
+ , Space
+ , Str "if"
+ , Space
+ , Str "the"
+ , Space
+ , Str "note"
+ , Space
+ , Str "contains"
+ , Space
+ , Str "multiple"
+ , Space
+ , Str "spans"
+ , Space
+ , Str "with"
+ , Space
+ , Str "identifiers?"
+ , Space
+ , Str "\8594"
+ , Space
+ , Str "See"
+ , Space
+ , Span
+ ( "" , [ "tcrf" ] , [] )
+ [ RawInline
+ (Format "opendocument")
+ "p.\160000"
+ ]
+ , Str "."
+ ]
+ ]
+ , [ Plain
+ [ Str "What"
+ , Space
+ , Str "happens"
+ , Space
+ , Str "if"
+ , Space
+ , Str "a"
+ , Space
+ , Str "span"
+ , Space
+ , Str "in"
+ , Space
+ , Str "contained"
+ , Space
+ , Str "in"
+ , Space
+ , Str "a"
+ , Space
+ , Str "span?"
+ , Space
+ , Str "\8594"
+ , Space
+ , Str "See"
+ , Space
+ , Span
+ ( "" , [ "tcrf" ] , [] )
+ [ RawInline
+ (Format "opendocument")
+ "p.\160000"
+ ]
+ , Str "."
+ ]
+ ]
+ , [ Plain
+ [ Str "What"
+ , Space
+ , Str "are"
+ , Space
+ , Str "the"
+ , Space
+ , Str "notes?"
+ , Space
+ , Str "\8594"
+ , Space
+ , Span
+ ( "" , [ "tcrf" ] , [ ( "reftype" , "note" ) ] )
+ [ RawInline
+ (Format "opendocument")
+ "nn.\160000, 000 and 000"
+ ]
+ ]
+ ]
+ , [ Plain
+ [ Str "Where"
+ , Space
+ , Str "are"
+ , Space
+ , Str "the"
+ , Space
+ , Str "notes?"
+ , Space
+ , Str "\8594"
+ , Space
+ , Span
+ ( "" , [ "tcrf" ] , [] )
+ [ RawInline
+ (Format "opendocument")
+ "pp.\160000 and 000"
+ ]
+ ]
+ ]
+ ]
+, Para [ Str "" , Span ( "toc-notes-end" , [] , [] ) [] ]
+, Para
+ [ Str ""
+ , Span
+ ( "doubledlbl" , [] , [ ( "refanchor" , "both" ) ] )
+ [ Str ""
+ , Span ( "doubledlbl-beg" , [] , [] ) []
+ , Str "A"
+ , Space
+ , Str "portion"
+ , Space
+ , Str "of"
+ , Space
+ , Str "text"
+ , Space
+ , Str "that"
+ , Space
+ , Str "may"
+ , Space
+ , Str "cross"
+ , Space
+ , Str "a"
+ , Space
+ , Str "page"
+ , Space
+ , Str "break."
+ , Str ""
+ , Span ( "doubledlbl-end" , [] , [] ) []
+ ]
+ ]
+, Para
+ [ Str ""
+ , Span
+ ( "lblatend" , [] , [ ( "refanchor" , "end" ) ] )
+ [ Str "And"
+ , Space
+ , Str "this"
+ , Space
+ , Str "one"
+ , Space
+ , Str "is"
+ , Space
+ , Str "labelized"
+ , Space
+ , Str "at"
+ , Space
+ , Str "the"
+ , Space
+ , Str "end."
+ , Str ""
+ , Span ( "lblatend" , [] , [] ) []
+ ]
+ ]
+, Para
+ [ Str "See"
+ , Space
+ , Span
+ ( "" , [ "tcrf" ] , [] )
+ [ RawInline
+ (Format "opendocument")
+ "pp.\160000\8211000"
+ ]
+ , Str "."
+ ]
+]
diff --git a/text-crossrefs/test/test-functions.lua b/text-crossrefs/test/test-functions.lua
new file mode 100644
index 0000000..e38bc95
--- /dev/null
+++ b/text-crossrefs/test/test-functions.lua
@@ -0,0 +1,36 @@
+local refs
+
+refs = parse_references_enum('mylabel')
+assert(#refs == 1)
+assert(refs[1].anchor == 'mylabel')
+assert(not refs[1].end_of_range)
+assert(make_raw_content_tex(refs, 'page', true)
+ == '\\crossrefenum{{mylabel}}')
+assert(make_raw_content_tex(refs, 'page', false)
+ == '\\crossrefenum[noprefix]{{mylabel}}')
+assert(make_raw_content_tex(refs, 'note', true)
+ == '\\crossrefenum[note]{{mylabel}}')
+assert(make_raw_content_tex(refs, 'note', false)
+ == '\\crossrefenum[note][noprefix]{{mylabel}}')
+
+refs = parse_references_enum('rangebeg>rangeend')
+assert(#refs == 1)
+assert(refs[1].anchor == 'rangebeg')
+assert(refs[1].end_of_range == 'rangeend')
+assert(make_raw_content_tex(refs, 'page', true)
+ == '\\crossrefenum{{rangebeg to rangeend}}')
+
+refs = parse_references_enum('first, second')
+assert(#refs == 2)
+assert(refs[1].anchor == 'first')
+assert(refs[2].anchor == 'second')
+assert(make_raw_content_tex(refs, 'page', true)
+ == '\\crossrefenum{{first}{second}}')
+
+refs = parse_references_enum('first, rangebeg>rangeend')
+assert(#refs == 2)
+assert(refs[1].anchor == 'first')
+assert(refs[2].anchor == 'rangebeg')
+assert(refs[2].end_of_range == 'rangeend')
+assert(make_raw_content_tex(refs, 'page', true)
+ == '\\crossrefenum{{first}{rangebeg to rangeend}}')
diff --git a/text-crossrefs/text-crossrefs.lua b/text-crossrefs/text-crossrefs.lua
new file mode 100644
index 0000000..d9f631f
--- /dev/null
+++ b/text-crossrefs/text-crossrefs.lua
@@ -0,0 +1,611 @@
+local stringify = pandoc.utils.stringify
+
+local TEXT_CROSSREF_CLASS = 'tcrf'
+local REF_TYPE_ATTR = 'reftype'
+local PREFIXED_ATTR = 'prefixref'
+local PLACE_LABEL_ATTR = 'refanchor'
+local IS_CONFIG_ARRAY = { ['additional_types'] = true }
+local RAW_ATTRIBUTE
+
+-- ConTeXt-specific tweak in order to add the label to the footnote
+--[[
+ Placing the label in square brackets immediatly after \footnote
+ in the regular way would require unpacking the content
+ of the Note and wrapping them with the RawInlines
+ '\footnote[note:' .. label .. ']{' and '}'.
+ However, Notes have the strange property of being Inlines
+ that contain Blocks, so this would result in Blocks being
+ brought into the content of the object that contains the Note,
+ which would be invalid.
+ That's why we place the label at the end of the \footnote
+ and redefine the macro so that it takes it into account.
+]]--
+
+local function support_footnote_label_ConTeXt(metadata)
+ if RAW_ATTRIBUTE == 'context' then
+ local label_macro_def = '\n\\def\\withfirstopt[#1]#2{#2[#1]}\n'
+ if not metadata['header-includes'] then
+ metadata['header-includes'] = pandoc.MetaBlocks(pandoc.RawBlock('context', ''))
+ end
+ metadata['header-includes']:insert(pandoc.RawBlock('context', label_macro_def))
+ end
+ return metadata
+end
+
+-- Configuration
+
+local function define_raw_attribute()
+ if FORMAT == 'native' then
+ RAW_ATTRIBUTE = pandoc.system.environment().TESTED_FORMAT
+ elseif FORMAT == 'docx' then
+ RAW_ATTRIBUTE = 'openxml'
+ elseif FORMAT == 'odt' or FORMAT == 'opendocument' then
+ RAW_ATTRIBUTE = 'opendocument'
+ elseif FORMAT == 'context' or FORMAT == 'latex' then
+ RAW_ATTRIBUTE = FORMAT
+ else
+ error(FORMAT ..
+ ' output not supported by text-crossrefs.lua.')
+ end
+end
+
+local function define_label_template()
+ if RAW_ATTRIBUTE == 'opendocument' or RAW_ATTRIBUTE == 'openxml' then
+ IS_LABEL_SET_BY_PANDOC = true
+ elseif RAW_ATTRIBUTE == 'context' then
+ if PANDOC_VERSION < pandoc.types.Version('2.14') then
+ LABEL_TEMPLATE = '\\pagereference[{{label}}]'
+ else
+ IS_LABEL_SET_BY_PANDOC = true
+ end
+ elseif RAW_ATTRIBUTE == 'latex' then
+ LABEL_TEMPLATE = '\\label{{{label}}}'
+ end
+end
+
+local config = {
+ page_prefix = 'p. ',
+ pages_prefix = 'pp. ',
+ note_prefix = 'n. ',
+ notes_prefix = 'nn. ',
+ pagenote_first_type = 'page',
+ pagenote_separator = ', ',
+ pagenote_at_end = '',
+ pagenote_factorize_first_prefix_in_enum = 'no',
+ multiple_delimiter = ', ',
+ multiple_before_last = ' and ',
+ references_range_separator = '>',
+ range_separator = '–',
+ references_enum_separator = ', ',
+ only_explicit_labels = 'false',
+ default_reftype = 'page',
+ default_prefixref = 'yes',
+ filelabel_ref_separator = '::',
+ range_delim_crossrefenum = ' to ',
+ additional_types = {}
+}
+
+local accepted_types = {
+ page = true,
+ note = true,
+ pagenote = true
+}
+
+local function format_config_to_openxml()
+ local to_format = { 'page_prefix',
+ 'pages_prefix',
+ 'note_prefix',
+ 'notes_prefix',
+ 'pagenote_separator',
+ 'pagenote_at_end',
+ 'range_separator',
+ 'multiple_delimiter',
+ 'multiple_before_last' }
+ for i = 1, #to_format do
+ config[to_format[i]] = '' ..
+ config[to_format[i]] .. ''
+ end
+end
+
+local function set_configuration_item_from_metadata(item, metamap)
+ metakey = 'tcrf-' .. string.gsub(item, '_', '-')
+ if metamap[metakey] then
+ if IS_CONFIG_ARRAY[item] then
+ -- The metadata values is a list of MetaInlines,
+ -- each of them contains a single Str.
+ for _, value_metalist in ipairs(metamap[metakey]) do
+ table.insert(config[item], value_metalist[1].text)
+ end
+ else
+ -- The metadata value is a single Str in a MetaInlines.
+ config[item] = metamap[metakey][1].text
+ end
+ end
+end
+
+local function configure(metadata)
+ define_raw_attribute()
+ define_label_template()
+ for item, _ in pairs(config) do
+ set_configuration_item_from_metadata(item, metadata)
+ end
+ if RAW_ATTRIBUTE == 'openxml' then
+ format_config_to_openxml()
+ end
+ if RAW_ATTRIBUTE == 'context' or RAW_ATTRIBUTE == 'latex' then
+ for _, additional_type in ipairs(config.additional_types) do
+ accepted_types[additional_type] = true
+ end
+ end
+end
+
+-- End of configuration
+
+-- Preprocessing of identifiers on notes
+-- Necessary for those output format where a note can be referred to
+-- only via an identifier directly attached to it, not to its content
+
+local spans_to_note_labels = {}
+local current_odt_note_index = 0
+local is_first_span_in_note = true
+local current_note_label
+local text_to_note_labels = {}
+
+local function map_span_to_label(span)
+ if RAW_ATTRIBUTE == 'opendocument' then
+ spans_to_note_labels[span.identifier] = 'ftn' .. current_odt_note_index
+ elseif RAW_ATTRIBUTE == 'openxml' or RAW_ATTRIBUTE == 'context' then
+ if is_first_span_in_note then
+ current_note_label = span.identifier
+ is_first_span_in_note = false
+ end
+ spans_to_note_labels[span.identifier] = current_note_label
+ end
+end
+
+local function map_spans_to_labels(container)
+ for i = 1, #container.content do
+ -- The tests must be separate in order to support spans inside spans.
+ if container.content[i].t == 'Span'
+ and container.content[i].identifier
+ then
+ map_span_to_label(container.content[i])
+ end
+ if container.content[i].content then
+ map_spans_to_labels(container.content[i])
+ end
+ end
+end
+
+local function map_spans_to_notelabels(note)
+ if RAW_ATTRIBUTE == 'context'
+ or RAW_ATTRIBUTE == 'opendocument'
+ or RAW_ATTRIBUTE == 'openxml'
+ then
+ is_first_span_in_note = true
+ map_spans_to_labels(note)
+ current_odt_note_index = current_odt_note_index + 1
+ end
+end
+
+local function control_label_placement(span)
+ local label_placement = span.attributes[PLACE_LABEL_ATTR]
+ if label_placement then
+ local id = span.identifier
+ if label_placement == 'end' then
+ span.content:insert(pandoc.Span({}, { id = id }))
+ span.identifier = nil
+ elseif label_placement == 'both' then
+ span.content:insert(1, pandoc.Span({}, { id = id .. '-beg' })) -- for DOCX/ODT
+ span.content:insert(pandoc.Span({}, { id = id .. '-end' }))
+ span.identifier = nil
+ elseif label_placement ~= 'beg' then
+ error('Invalid value ' .. label_placement .. ' on attribute ' .. PLACE_LABEL_ATTR .. ': ' ..
+ 'shoud be “beg” (default), “end” of “both”.')
+ end
+ end
+ return span
+end
+
+local function make_label(label)
+ -- pandoc.Null() cannot be used here because it is a Block element.
+ local label_pandoc_object = pandoc.Str('')
+ if not IS_LABEL_SET_BY_PANDOC then
+ local label_rawcode = string.gsub(LABEL_TEMPLATE, '{{label}}', label)
+ label_pandoc_object = pandoc.RawInline(RAW_ATTRIBUTE, label_rawcode)
+ end
+ return label_pandoc_object
+end
+
+local function labelize_span(span)
+ if span.identifier ~= '' then
+ local label = span.identifier
+ local label_begin = make_label(label, 'begin')
+ return { label_begin, span }
+ end
+end
+
+local current_note_labels = {}
+
+local collect_note_labels = {
+ Span = function(span)
+ if span.identifier ~= ''
+ and (config.only_explicit_labels == 'false' or span.classes:includes('label'))
+ then
+ table.insert(current_note_labels, span.identifier)
+ end
+ end
+}
+
+local function make_notelabel(pos)
+ -- About the strategy followed with ConTeXt,
+ -- see above support_footnote_label_ConTeXt.
+ local raw_code = ''
+ if pos == 'begin' then
+ if RAW_ATTRIBUTE == 'openxml' then
+ raw_code = string.gsub(
+ '',
+ '{{label}}', current_note_labels[1])
+ elseif RAW_ATTRIBUTE == 'context' then
+ raw_code = '\\withfirstopt[note:' .. current_note_labels[1] .. ']'
+ end
+ elseif pos == 'end' then
+ if RAW_ATTRIBUTE == 'openxml' then
+ raw_code = string.gsub('',
+ '{{label}}', current_note_labels[1])
+ end
+ end
+ return pandoc.RawInline(RAW_ATTRIBUTE, raw_code)
+end
+
+local function labelize_note(note)
+ local labelized_note
+ local label_begin = make_notelabel('begin')
+ local label_end = make_notelabel('end')
+ return { label_begin, note, label_end }
+end
+
+local function map_text_to_note_labels(current_note_labels)
+ local note_label = 'note:' .. current_note_labels[1]
+ for _, text_label in ipairs(current_note_labels) do
+ text_to_note_labels[text_label] = note_label
+ end
+end
+
+function set_notelabels(note)
+ current_note_labels = {}
+ pandoc.walk_inline(note, collect_note_labels)
+ if #current_note_labels > 0 then
+ map_text_to_note_labels(current_note_labels)
+ return labelize_note(note)
+ end
+end
+
+-- End of preprocessing of identifiers on notes
+
+-- Gathering of data from the references span
+
+local function new_ref(anchor, end_of_range)
+ -- A ref is a string-indexed table containing an "anchor" field
+ -- and an optionnal "end_of_range" field.
+ -- When "end_of_range" is non-nil, the ref is a range.
+ local ref = {}
+ ref.anchor = anchor
+ ref.end_of_range = end_of_range
+ return ref
+end
+
+local function is_ref_external(raw_references)
+ if string.find(raw_references, config.filelabel_ref_separator, 1, true) then
+ return true
+ else
+ return false
+ end
+end
+
+local function parse_possible_range(reference)
+ -- If reference is a string representing a range,
+ -- returns the strings representing the boundaries of the range.
+ -- Else, returns the string.
+ local range_first, range_second = nil
+ local delim_beg, delim_end = string.find(reference,
+ config.references_range_separator,
+ 1, true)
+ if delim_beg then
+ range_first = string.sub(reference, 1, delim_beg - 1)
+ range_second = string.sub(reference, delim_end + 1)
+ end
+ return (range_first or reference), range_second
+end
+
+local function parse_next_reference(raw_references, beg_of_search)
+ -- Returns the ref corresponding to the next reference string
+ -- and the index which the parsing should be resumed at.
+ local current_ref = false
+ local next_ref_beg = nil
+ if beg_of_search < #raw_references then
+ -- The delimiter can be composed of more than one character.
+ local delim_beg, delim_end = string.find(raw_references,
+ config.references_enum_separator,
+ beg_of_search, true)
+ if delim_beg then
+ reference = string.sub(raw_references, beg_of_search, delim_beg - 1)
+ next_ref_beg = delim_end + 1
+ else
+ reference = string.sub(raw_references, beg_of_search)
+ next_ref_beg = #raw_references
+ end
+ current_ref = new_ref(parse_possible_range(reference))
+ end
+ return current_ref, next_ref_beg
+end
+
+local function parse_references_enum(raw_references)
+ -- raw_refs is a string consisting of a list of single references or ranges.
+ -- Returns an array of refs produced by "new_ref" above.
+ local parsed_refs = {}
+ local current_ref, next_ref_beg = parse_next_reference(raw_references, 1)
+ while current_ref do
+ table.insert(parsed_refs, current_ref)
+ current_ref, next_ref_beg =
+ parse_next_reference(raw_references, next_ref_beg)
+ end
+ return parsed_refs
+end
+
+local function error_on_attr(attr_key, attr_value, span_content)
+ error('Invalid value "' .. attr_value .. '" for attribute "' .. attr_key ..
+ '" in the span with class "' .. TEXT_CROSSREF_CLASS ..
+ '" whose content is "' .. stringify(span_content) .. '".')
+end
+
+local function get_ref_type(span)
+ local ref_type = span.attributes[REF_TYPE_ATTR] or config.default_reftype
+ if not accepted_types[ref_type] then
+ error_on_attr(REF_TYPE_ATTR, ref_type, span.content)
+ end
+ return ref_type
+end
+
+local function if_prefixed(span)
+ local prefixed_attr_value = span.attributes[PREFIXED_ATTR] or config.default_prefixref
+ if prefixed_attr_value ~= 'yes' and prefixed_attr_value ~= 'no' then
+ error_on_attr(PREFIXED_ATTR, prefixed_attr_value, span.content)
+ end
+ local is_prefixed = true
+ if prefixed_attr_value == 'no' then is_prefixed = false end
+ return is_prefixed
+end
+
+-- End of gathering of data from the references span
+
+-- Formatting references as raw inlines.
+
+local function make_crossrefenum_first_arg(ref_type)
+ local ref_type_is_explicit = ref_type ~= config.default_reftype
+ local crossrefenum_first_arg = ''
+ if ref_type_is_explicit then
+ crossrefenum_first_arg = '[' .. ref_type .. ']'
+ end
+ return crossrefenum_first_arg
+end
+
+local function make_crossrefenum_second_arg(is_prefixed)
+ local is_prefixed_is_explicit = is_prefixed ~= (config.default_prefixref == 'yes')
+ local crossrefenum_second_arg = ''
+ local is_prefixed_string = ''
+ if is_prefixed_is_explicit then
+ if is_prefixed then
+ is_prefixed_string = 'withprefix'
+ else
+ is_prefixed_string = 'noprefix'
+ end
+ crossrefenum_second_arg = '[' .. is_prefixed_string .. ']'
+ end
+ return crossrefenum_second_arg
+end
+
+local function make_crossrefenum_references_list(refs, ref_type)
+ local crossrefenum_references_list = ''
+ for i = 1, #refs do
+ local ref = refs[i]
+ local anchor = ref.anchor
+ if FORMAT == 'context'
+ and (ref_type == 'note' or ref_type == 'pagenote')
+ then
+ anchor = text_to_note_labels[anchor]
+ end
+ local texified_ref = '{' .. anchor
+ if ref.end_of_range then
+ texified_ref = texified_ref .. config.range_delim_crossrefenum .. ref.end_of_range
+ end
+ texified_ref = texified_ref .. '}'
+ crossrefenum_references_list = crossrefenum_references_list .. texified_ref
+ end
+ return crossrefenum_references_list
+end
+
+local function make_raw_content_tex(refs, ref_type, is_prefixed)
+ local texified_references = ''
+ texified_references = '\\crossrefenum'
+ .. make_crossrefenum_first_arg(ref_type)
+ .. make_crossrefenum_second_arg(is_prefixed)
+ .. '{' .. make_crossrefenum_references_list(refs, ref_type) .. '}'
+ return texified_references
+end
+
+local function make_prefix_xml(ref_type, is_plural)
+ local prefix = ''
+ if is_plural then
+ prefix = config[ref_type .. 's_prefix']
+ else
+ prefix = config[ref_type .. '_prefix']
+ end
+ return prefix
+end
+
+local function make_page_reference_xml(target, is_prefixed)
+ local xml_page_ref = ''
+ if is_prefixed then
+ xml_page_ref = make_prefix_xml('page', false)
+ end
+ if RAW_ATTRIBUTE == 'opendocument' then
+ xml_page_ref = xml_page_ref ..
+ '000'
+ elseif RAW_ATTRIBUTE == 'openxml' then
+ xml_page_ref = xml_page_ref ..
+ '' ..
+ ' PAGEREF ' ..
+ target .. ' \\h ' ..
+ '' ..
+ '000' ..
+ ''
+ end
+ return xml_page_ref
+end
+
+local function make_pagerange_reference_xml(first, second, is_prefixed)
+ local prefix = ''
+ if is_prefixed then prefix = make_prefix_xml('page', true) end
+ return prefix .. make_page_reference_xml(first, false) ..
+ config.range_separator .. make_page_reference_xml(second, false)
+end
+
+local function make_note_reference_xml(target, is_prefixed)
+ local note_ref_xml = ''
+ if is_prefixed then
+ note_ref_xml = make_prefix_xml('note', false)
+ end
+ if RAW_ATTRIBUTE == 'opendocument' then
+ note_ref_xml = note_ref_xml ..
+ '000'
+ elseif RAW_ATTRIBUTE == 'openxml' then
+ note_ref_xml = note_ref_xml ..
+ '' ..
+ ' NOTEREF ' ..
+ (spans_to_note_labels[target] or '') .. '_Note' .. ' \\h ' ..
+ '' ..
+ '000' ..
+ ''
+ end
+ return note_ref_xml
+end
+
+local function make_pagenote_reference_xml(target, is_prefixed)
+ local pagenote_ref_xml = ''
+ if is_prefixed then
+ pagenote_ref_xml = make_prefix_xml(config.pagenote_first_type, false)
+ end
+ if config.pagenote_first_type == 'page' then
+ pagenote_ref_xml = pagenote_ref_xml ..
+ make_page_reference_xml(target, false) ..
+ config.pagenote_separator .. make_note_reference_xml(target, true) ..
+ config.pagenote_at_end
+ elseif config.pagenote_first_type == 'note' then
+ pagenote_ref_xml = pagenote_ref_xml ..
+ make_note_reference_xml(target, false) ..
+ config.pagenote_separator .. make_page_reference_xml(target, true) ..
+ config.pagenote_at_end
+ else
+ error('“tcrf-pagenote-first-type” must be set either to “page” or “note”.')
+ end
+ return pagenote_ref_xml
+end
+
+local function make_reference_xml(ref, ref_type, is_prefixed)
+ local reference_xml = ''
+ if ref_type == 'page' and ref.end_of_range then
+ reference_xml =
+ make_pagerange_reference_xml(ref.anchor, ref.end_of_range, is_prefixed)
+ elseif ref_type == 'page' then
+ reference_xml = make_page_reference_xml(ref.anchor, is_prefixed)
+ elseif ref_type == 'note' then
+ reference_xml = make_note_reference_xml(ref.anchor, is_prefixed)
+ elseif ref_type == 'pagenote' then
+ reference_xml = make_pagenote_reference_xml(ref.anchor, is_prefixed)
+ end
+ return reference_xml
+end
+
+local function make_global_prefix_xml(ref_type)
+ local global_prefix_xml = ''
+ local prefix_type = ref_type
+ if ref_type == 'pagenote' then
+ prefix_type = config.pagenote_first_type
+ end
+ global_prefix_xml = make_prefix_xml(prefix_type, true)
+ return global_prefix_xml
+end
+
+local function make_references_xml(refs, ref_type, is_prefixed)
+ local references_xml = ''
+ for i = 1, #refs do
+ references_xml = references_xml ..
+ make_reference_xml(refs[i], ref_type, is_prefixed)
+ if i < #refs then
+ if i < #refs - 1 then
+ references_xml = references_xml .. config.multiple_delimiter
+ else
+ references_xml = references_xml .. config.multiple_before_last
+ end
+ end
+ end
+ return references_xml
+end
+
+local function make_raw_content_xml(refs, ref_type, is_prefixed)
+ local is_enumeration = #refs > 1
+ local global_prefix = ''
+ if is_enumeration and is_prefixed
+ and (ref_type ~= 'pagenote'
+ or pagenote_factorize_first_prefix_in_enum == 'yes')
+ then
+ global_prefix = make_global_prefix_xml(ref_type)
+ is_prefixed = false
+ end
+ local references_raw_xml = make_references_xml(refs, ref_type, is_prefixed)
+ return global_prefix .. references_raw_xml
+end
+
+local function make_raw_content(refs, ref_type, is_prefixed)
+ local raw_content = ''
+ if RAW_ATTRIBUTE == 'context' or RAW_ATTRIBUTE == 'latex' then
+ raw_content = make_raw_content_tex(refs, ref_type, is_prefixed)
+ else
+ raw_content = make_raw_content_xml(refs, ref_type, is_prefixed)
+ end
+ return raw_content
+end
+
+local function format_references(refs, ref_type, is_prefixed)
+ local raw_content = make_raw_content(refs, ref_type, is_prefixed)
+ return pandoc.RawInline(RAW_ATTRIBUTE, raw_content)
+end
+
+local function format_enum(span)
+ -- A reference is a Str contained in a span representing a label or a range of labels.
+ -- A ref is a ref object produced by the function "new_ref" defined above.
+ if span.classes:includes(TEXT_CROSSREF_CLASS)
+ and not(is_ref_external(stringify(span.content)))
+ then
+ local refs = parse_references_enum(stringify(span.content))
+ local ref_type = get_ref_type(span)
+ local is_prefixed = if_prefixed(span)
+ span.content = format_references(refs, ref_type, is_prefixed)
+ end
+ return span
+end
+
+return {
+ { Meta = configure },
+ { Meta = support_footnote_label_ConTeXt },
+ { Note = set_notelabels },
+ { Note = map_spans_to_notelabels },
+ { Span = control_label_placement },
+ { Span = labelize_span },
+ { Span = format_enum }
+}