Include margin-notes.lua

This commit is contained in:
Bastien Dumont 2022-04-03 17:38:42 +02:00
parent 74d8d18801
commit 04f3ef7267
5 changed files with 957 additions and 0 deletions

23
margin-notes/Makefile Normal file
View File

@ -0,0 +1,23 @@
.PHONY: test test-internal test-fr test-en
SHELL=/bin/bash
nblines := $(shell wc -l margin-notes.lua | cut -d ' ' -f 1)
but_four_last_lines := $(shell echo $$(($(nblines) - 4)))
test: test test-internal
test-internal: margin-notes.lua test/test-functions.lua
-rm --interactive=never test/tmp.lua
sed -n 1,$(but_four_last_lines)p margin-notes.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: margin-notes.lua test/test.md
pandoc -t native -L margin-notes.lua test/test.md > test/tmp.native
diff test/tmp.native test/test.native
@echo -e '\n===============\ntest passed.\n===============\n'
rm test/tmp.native

132
margin-notes/README.md Normal file
View File

@ -0,0 +1,132 @@
# Marginal notes in Pandoc for semantically marked spans of text
`margin-notes` enables you to create unnumbered marginal notes from spans with user-defined class and properties. It currently only works with LaTeX, ConTeXt and, to a limited extent, DOCX (using tooltips instead of marginal notes) and ODT (using annotations).
## Defining the renderings
This filter does not provide a predefined class for marginal notes. Instead, it lets the user choose what classes it will handle and how. This way, it is possible to define appropriate renderings of spans marked with different, semantically meaningful classes.
Here is an example. Please note that it is necessary to surround the values both with single quotes and backsticks to force Pandoc to pass them unchanged to `margin-notes.lua`.
``` yaml
mrgnn-define-renderings:
- class: term
body-text: '`**§content§**`'
note-text: '`§attr.lem§: *§attr.def§*`'
- class: warning
body-text: '`**!**`'
note-text: '`**§content§**`'
```
The values of `note-text` and `body-text` are markdown templates. Inside it, you have access to the content and the attributes of the span with the variables `§content§` and `§attr.X§`. The special character `§` should be escaped with a backslash if you want it to be treated literally.
Mardown in the content and the attributes of the span is taken into account whenever possible. Otherwise (e.g. when used in the target of a link or the path of an image), the values of `§content§` and `§attr.X§` are converted to a plain string without any formatting.
The preceding configuration can be applied to the following sample:
``` markdown
[Important!]{.warning}You can use LaTeX to include
[BibTeX]{.term
lem=BibTeX
def="A program designed to format bibliographical entries"
} citations. Note that in LaTeX environments,
the material between the begin and end tags will be interpreted
as [raw LaTeX]{.term
lem="Raw code"
def="Code inserted **untouched** in the output."
}, not as Markdown.
```
For DOCX output, the note is set as the content of a tooltip attached to an asterisk following the body text. Since this does not always produce good results, notably because tooltips only display plain strings, DOCX output is deactivated by default. However, you can activate it for individual classes by setting the variables `docx-body-text` and `docx-note-text`:
``` yaml
mrgnn-define-renderings:
- class: term
body-text: '`**§content§**`'
note-text: '`§attr.lem§: *§attr.def§*`'
docx-body-text: '`**§content§**`'
docx-note-text: '`§attr.lem§: §attr.def§`'
```
With ODT, the marginal notes are always converted to annotations. By default, the values of `body-text` and `note-text` are used. However, since LibreOffice (at least) does not support all kinds of formatting in annotations, you can define specific values for `odt-body-text` and `odt-note-text` as well.
## Tips and tricks
### Simplify your markdown file
So as not to overload your markdown file, you may wish to pre-process your spans with a custom filter. For instance, the sample above could be simplified like this:
``` markdown
[Important!]{.warning}You can use LaTeX to include [BibTeX]{.term}
citations. Note that in LaTeX environments,
the material between the begin and end tags will be interpreted
as [raw LaTeX]{.term lem="Raw code"}, not as Markdown.
```
The following filter, to be applied before `margin-notes.lua`, adds the required attributes to the spans with class `term`.
``` lua
local definitions = {
BibTeX = 'A program designed to format bibliographical entries.',
['Raw code'] = 'Code inserted untouched in the output.'
}
function Span(span)
if span.classes:includes('term') then
attr = span.attributes
if not attr.lem then
attr.lem = pandoc.utils.stringify(span.content)
end
if not attr.def then
attr.def = definitions[attr.lem]
end
return span
end
end
```
### Further customization of the marginal notes
By default, all marginal notes are rendered by the macro `\mrgnn`, which is defined as follows:
* LaTeX:
``` tex
\newcommand{\mrgnn}[1]{%
\marginpar{{\footnotesize #1}}%
}
```
* ConTeXt (adapted from <https://wiki.contextgarden.net/Footnotes#Footnotes_in_the_margin>):
``` tex
\define\placeMrgnn{%
\inoutermargin{\vtop{%
\placelocalnotes[mrgnn][before=,after=]%
}}%
}
\definenote
[mrgnn]
[location=text,
bodyfont=x,
next=\placeMrgnn]
\setupnotation
[mrgnn]
[number=no,
alternative=serried]
```
However, you can associate different macros with individual classes by setting the variable `csname`:
``` yaml
mrgnn-define-renderings:
- class: term
body-text: '`**§content§**`'
note-text: '`§attr.lem§: *§attr.def§*`'
csname: mrgnnTerm
```
In that case, you will have to define `\mrgnnTerm` in your LaTeX or ConTeXt template. This macro takes one argument, which is the result of the processing of `note-text`.

View File

@ -0,0 +1,533 @@
-- TODO : body-text est utilisé comme contenu de §content§
-- (visible si on remplace '' par quelque chose dans la config de warning)
local find_in_string = string.find
local gsub = string.gsub
local get_substring = string.sub
local unpack_table = table.unpack
local insert_in_table = table.insert
local table_pop = table.remove
local pandoc_to_string = pandoc.utils.stringify
-- No char in the placeholder string should have to be escaped
-- when found in a URL or in a Lua pattern.
local PLACEHOLDER_LABEL = 'MRGNN_PLACEHOLDER'
local PLACEHOLDER_BEGIN = 'BEG_'
local PLACEHOLDER_END = '_END'
local PLACEHOLDER_REGEX = PLACEHOLDER_BEGIN ..
PLACEHOLDER_LABEL .. '(.-)' ..
PLACEHOLDER_LABEL .. PLACEHOLDER_END
-- String indicating an indefined value in the output
local UNDEFINED = '??'
local DOCX_TOOLTIP_ANCHOR = '*'
local DOCX_TOOLTIP_ANCHOR_SYTLE = 'Tooltip anchor'
local TEMPLATE_VARIABLE_MARKUP_CHAR = '§'
local mc = TEMPLATE_VARIABLE_MARKUP_CHAR
local TEMPLATE_VAR_REGEX =
'%f[\\' .. mc .. ']' .. mc .. '(.-)%f[\\' .. mc .. ']' .. mc
local DEFAULT_MACROS = {
-- ConTeXt code adapted from
-- https://wiki.contextgarden.net/Footnotes#Footnotes_in_the_margin
context =
'\\define\\placeMrgnn{%\n' ..
' \\inoutermargin{\\vtop{%\n' ..
' \\placelocalnotes[mrgnn][before=,after=]%\n' ..
' }}%\n' ..
'}\n' ..
'\n' ..
'\\definenote\n' ..
' [mrgnn]\n' ..
' [location=text,\n' ..
' bodyfont=x,\n' ..
' next=\\placeMrgnn]\n' ..
'\n' ..
'\\setupnotation\n' ..
' [mrgnn]\n' ..
' [number=no,\n' ..
' alternative=serried]',
latex =
'\\newcommand{\\mrgnn}[1]{%\n' ..
' \\marginpar{{\\footnotesize #1}}%\n' ..
'}'
}
if FORMAT == 'odt' then FORMAT = 'opendocument'
elseif FORMAT == 'docx' then FORMAT = 'openxml' end
local function copy_unidimensional_table(the_table)
return { unpack_table(the_table) }
end
local function add_copy_to_list(value, list)
-- Value is of any type except thread and userdata.
local to_be_added
if type(value) == 'table' then
to_be_added = copy_unidimensional_table(value)
else
to_be_added = value
end
insert_in_table(list, to_be_added)
end
local function to_bool(var)
return not not var
end
local function inlines_to_string(list)
return pandoc_to_string(pandoc.Para(list))
end
local config = {}
function add_rendering(config, class,
body_text, note_text,
docx_body_text, docx_note_text,
odt_body_text, odt_note_text,
csname)
-- The values are plain strings (not the empty string!) or nil.
config[class] = {
body_text = body_text,
note_text = note_text,
docx_body_text = docx_body_text,
docx_note_text = docx_note_text,
odt_body_text = odt_body_text,
odt_note_text = odt_note_text,
csname = csname
}
end
local function meta_to_str(meta_obj)
if meta_obj and meta_obj[1] then
return meta_obj[1].text
else
return nil
end
end
local function get_renderings_config(meta)
for key, value in pairs(meta) do
if key == 'mrgnn-define-renderings' then
for i = 1, #value do
this_value = value[i]
add_rendering(config,
meta_to_str(this_value.class),
meta_to_str(this_value['body-text']),
meta_to_str(this_value['note-text']),
meta_to_str(this_value['docx-body-text']),
meta_to_str(this_value['docx-note-text']),
meta_to_str(this_value['odt-body-text']
or this_value['body-text']),
meta_to_str(this_value['odt-note-text']
or this_value['note-text']),
meta_to_str(this_value['csname'])
)
end
end
end
return meta
end
local function to_pandoc_inlines(markdown_str)
local inlines = {}
if markdown_str ~= '' then
local whole_doc = pandoc.read(markdown_str)
inlines = whole_doc.blocks[1].content
end
return inlines
end
local function template_to_pandoc_fragment(template)
-- The substitution is necessary in order to differentiate
-- the '§' characters that are part of placeholder markup
-- and the litteral ones.
template = gsub((template or ''), TEMPLATE_VAR_REGEX,
PLACEHOLDER_BEGIN .. PLACEHOLDER_LABEL ..
'%1' ..
PLACEHOLDER_LABEL .. PLACEHOLDER_END)
return to_pandoc_inlines(template)
end
local function has_children(elem)
return type(elem) == 'table' or type(elem) == 'userdata'
end
local function contains_placeholder(str)
-- Although it would speed up the process,
-- we don't exclude any string based on its key (e.g. "tag")
-- because users could use the same names for attributes.
local result = false
result = to_bool(find_in_string(str,
PLACEHOLDER_BEGIN .. PLACEHOLDER_LABEL ..
'.-'..
PLACEHOLDER_LABEL .. PLACEHOLDER_END))
return result
end
local function register_step_in_path(path, step)
insert_in_table(path, step)
end
local function unregister_last_step(path)
table_pop(path)
end
local function find_paths_to_placeholders(current_path, placeholders_paths, current_table)
--[[
current_path represents a path to a placeholder,
i.e. an unidimensional table of alternated numbers and strings
figuring the successive index and key values
at which the placeholder string is to be found in list.
list is a List of Inlines.
When two placeholders are to be found in the same string,
only one path is returned.
]]--
for index, elem in pairs(current_table) do
if has_children(elem) then
register_step_in_path(current_path, index)
find_paths_to_placeholders(current_path, placeholders_paths, elem)
unregister_last_step(current_path)
elseif type(elem) == 'string' then
if contains_placeholder(elem) then
register_step_in_path(current_path, index)
add_copy_to_list(current_path, placeholders_paths)
unregister_last_step(current_path)
end
end
end
end
local function get_paths_to_placeholders(list)
--[[
list is a List of Inlines
Returns a table of paths (see find_paths_to_placeholders).
If two placeholders are to be found in the same string,
then only one path is returned.
Example:
template = {
pandoc.SmallCaps(pandoc.Str(
PLACEHOLDER_BEGIN .. PLACEHOLDER_LABEL ..
'content' ..
PLACEHOLDER_LABEL .. PLACEHOLDER_END)),
pandoc.Str(':'), pandoc.Space(),
pandoc.Emph(pandoc.Str(
PLACEHOLDER_BEGIN .. PLACEHOLDER_LABEL ..
'attr.def' .. '.' ..
PLACEHOLDER_LABEL .. PLACEHOLDER_END))
}
return value = {
{ 1, 'content', 1, 'text' },
{ 4, 'content', 1, 'text' }
}
]]--
local placeholders_paths = {}
local current_path = {}
find_paths_to_placeholders(current_path, placeholders_paths, list)
return placeholders_paths
end
local function is_replacement_a_list(parent_object_type)
--[[
If parent_object_type is Str, this means that the object
containing the placeholder is an element in a list of Inlines.
In this case, the replacement should be a list of Inlines
destined to replace this element.
Otherwise (e.g. Link or Image), the replacement should be
a plain string.
]]--
return parent_object_type == 'Str'
end
local function get_replacement(placeholder,
instance_content, instance_attr,
replacement_is_list)
--[[
Returns a list of Inlines if replacement_is_list is true,
a plain string otherwise.
Markdown is interpreted in the first case only.
]]--
if placeholder == 'content' then
if replacement_is_list then
replacement = instance_content
else
replacement = inlines_to_string(instance_content)
end
elseif string.find(placeholder, '^attr%.') then
local key = get_substring(placeholder, #'attr.' + 1)
if replacement_is_list then
local replacement_markdown = instance_attr[key]
or '**' .. UNDEFINED .. '**'
replacement = to_pandoc_inlines(replacement_markdown)
else
replacement = instance_attr[key] or UNDEFINED
end
else
error('Invalid content "' .. placeholder .. '" in the value of a ' ..
'"body-text" or "note-text" metadata variable. ' ..
'It must either be "content" or begin with "attr".')
end
return replacement
end
local function get_strings_around_substring(s, i_beg, i_end)
local before = false
local after = false
if i_beg > 1 then
before = get_substring(s, 1, i_beg - 1)
end
if i_end < #s then
after = get_substring(s, i_end + 1)
end
return before, after
end
local function insert_strings_around_placeholder(replacement,
string_before_placeholder,
string_after_placeholder)
local replacement_is_list = type(replacement) == 'table'
if replacement_is_list then
if string_before_placeholder then
if replacement[1].t == 'Str' then
replacement[1].text = string_before_placeholder ..
replacement[1].text
else
insert_in_table(replacement, 1, pandoc.Str(string_before_placeholder))
end
end
if string_after_placeholder then
if replacement[#replacement].t == 'Str' then
replacement[#replacement].text = replacement[#replacement].text ..
string_after_placeholder
else
insert_in_table(replacement, pandoc.Str(string_after_placeholder))
end
end
else
replacement = (string_before_placeholder or '') ..
replacement ..
(string_after_placeholder or '')
end
return replacement
end
local function insert_replacement_in_elems(replacement,
pandoc_elems, i_object, placeholder_key,
i_placeholder_beg, i_placeholder_end)
local replacement_is_list = type(replacement) == 'table'
local string_with_placeholder = pandoc_elems[i_object][placeholder_key]
local string_before_placeholder, string_after_placeholder =
get_strings_around_substring(string_with_placeholder,
i_placeholder_beg, i_placeholder_end)
replacement = insert_strings_around_placeholder(replacement,
string_before_placeholder, string_after_placeholder)
if replacement_is_list then
for i = #replacement, 1, -1 do
insert_in_table(pandoc_elems, i_object + 1, replacement[i])
end
table_pop(pandoc_elems, i_object)
else
pandoc_elems[i_object][placeholder_key] = replacement
end
end
local function find_placeholders_in_string(str_with_placeholders)
local placeholders_data = {}
local i_data = 1
local i_beg, i_end, placeholder =
find_in_string(str_with_placeholders, PLACEHOLDER_REGEX)
while placeholder do
placeholders_data[i_data] = {
value = placeholder,
beg = i_beg,
['end'] = i_end
}
i_beg, i_end, placeholder =
find_in_string(str_with_placeholders, PLACEHOLDER_REGEX,
i_end)
i_data = i_data + 1
end
return placeholders_data
end
local function replace_placeholders_in_value(pandoc_elems,
i_object, placeholder_key,
instance_content, instance_attr)
--[[
Does not return anything: modifies instead pandoc_elems
by replacing the placeholder in pandoc_elems[key]
with the corresponding values from instance_content (a List of Inlines)
and instance_attr (a table of key/value pairs, where the values may
contain markdown formatting).
]]--
local str_with_placeholders = pandoc_elems[i_object][placeholder_key]
local replacement_is_list = is_replacement_a_list(pandoc_elems[i_object].t)
local placeholders_data = find_placeholders_in_string(str_with_placeholders)
for i = #placeholders_data, 1, -1 do
local placeholder_data = placeholders_data[i]
local replacement = get_replacement(placeholder_data.value,
instance_content, instance_attr,
replacement_is_list)
insert_replacement_in_elems(replacement,
pandoc_elems, i_object, placeholder_key,
placeholder_data.beg, placeholder_data['end'])
end
end
local function replace_placeholders(
-- table of Inlines, generally containing Str objects
-- whose text contains a placeholder
inlines_with_placeholders,
-- table of paths (see find_paths_to_placeholders for a definition)
paths_to_placeholders,
-- List of inlines
instance_content,
-- key-value table of attributes. The values may contain markdown.
instance_attr)
--[[
Replaces the Str objects in inlines_with_placeholders pointed at
by the paths in paths_to_placeholders with the data in
instance_content and instance_attr as required by the placeholder
strings.
If the placeholder string does not makes up the whole text of the Str,
create new Str containing the remaining chars.
Returns a new table containing the resulting Inlines.
]]--
if #paths_to_placeholders > 0 then
for i_path = #paths_to_placeholders, 1, -1 do
local path = paths_to_placeholders[i_path]
local current_scope = inlines_with_placeholders
local i_step = 1
local step = path[i_step]
while i_step < #path - 1 do
current_scope = current_scope[step]
i_step = i_step + 1
step = path[i_step]
end
local last_step = path[i_step + 1]
replace_placeholders_in_value(current_scope, step, last_step,
instance_content, instance_attr)
end
end
return inlines_with_placeholders
end
local function template_to_function(template)
--[[
inlines_with_placeholders cannot be memoized
for it is a reference to a table that will be changed
by replace_placeholders.
paths_to_placeholders can be memoized
because it is only traversed once it has been created.
]]--
if template then
local paths_to_placeholders
return
function(instance_content, instance_attr)
local inlines_with_placeholders = template_to_pandoc_fragment(template)
if not paths_to_placeholders then
paths_to_placeholders =
get_paths_to_placeholders(inlines_with_placeholders)
end
return replace_placeholders(
inlines_with_placeholders, paths_to_placeholders,
instance_content, instance_attr)
end
end
end
local function define_rendering_functions(meta)
local format_prefix = ''
if FORMAT == 'opendocument' then
format_prefix = 'odt_'
elseif FORMAT == 'openxml' then
format_prefix = 'docx_'
end
for class_name, class_config in pairs(config) do
config[class_name].render = {}
for _, part in ipairs({'body', 'note'}) do
config[class_name].render[part] =
template_to_function(class_config[format_prefix .. part .. '_text'])
end
end
end
local function set_macro_definition(meta)
if FORMAT == 'context' or FORMAT == 'latex' then
meta['header-includes'] = {
(meta['header-includes'] or pandoc.RawBlock(FORMAT, '')),
pandoc.RawBlock(FORMAT, DEFAULT_MACROS[FORMAT])
}
end
end
local function Meta(meta)
get_renderings_config(meta)
define_rendering_functions(meta)
set_macro_definition(meta)
return meta
end
local i_invocation = 0
local function wrap_in_raw_note_code(content, class_name)
-- content is a List of Inlines (output of replace_placeholders)
local margin_note = content
if FORMAT == 'context' or FORMAT == 'latex' then
local csname = config[class_name].csname or 'mrgnn'
insert_in_table(margin_note, 1, pandoc.RawInline(FORMAT, '\\' .. csname .. '{'))
insert_in_table(margin_note, pandoc.RawInline(FORMAT, '}'))
elseif FORMAT == 'openxml' then
i_invocation = i_invocation + 1
local bookmark_id = 'mrgnn_' .. i_invocation
margin_note = {
pandoc.RawInline(
FORMAT,
'<w:bookmarkStart w:id="' .. bookmark_id ..
'" w:name="' .. bookmark_id .. '"/>' ..
'<w:bookmarkEnd w:id="' .. bookmark_id .. '"/>' ..
'<w:hyperlink w:anchor="' .. bookmark_id .. '" ' ..
'w:tooltip="' .. pandoc_to_string(margin_note) .. '">'),
pandoc.Span(
pandoc.Str(DOCX_TOOLTIP_ANCHOR),
{ ['custom-style'] = DOCX_TOOLTIP_ANCHOR_SYTLE }),
pandoc.RawInline(
FORMAT,
'</w:hyperlink>')
}
elseif FORMAT == 'opendocument' then
insert_in_table(margin_note, 1, pandoc.RawInline(FORMAT,
'<office:annotation><text:p>'))
insert_in_table(margin_note, pandoc.RawInline(FORMAT,
'</text:p></office:annotation>'))
end
return margin_note
end
local function render_margin_notes(span)
for class_name, class_config in pairs(config) do
if span.classes:includes(class_name) then
local render_note = config[class_name].render.note
local render_body = config[class_name].render.body
local margin_note = {}
local body = {}
if render_note then
margin_note = wrap_in_raw_note_code(
render_note(span.content, span.attributes), class_name)
end
if render_body then
body = render_body(span.content, span.attributes)
end
span.content = body
return { span, unpack_table(margin_note) }
end
end
end
return {
{ Meta = Meta },
{ Span = render_margin_notes }
}

24
margin-notes/sample.md Normal file
View File

@ -0,0 +1,24 @@
---
mrgnn-define-renderings:
- class: term
body-text: '`**§content§**`'
note-text: '`§attr.lem§: *§attr.def§*`'
docx-body-text: '`**§content§**`'
docx-note-text: '`§attr.lem§: §attr.def§`'
- class: warning
body-text: ''
note-text: '`**§content§**`'
---
[Important!]{.warning}You can use LaTeX to include
[BibTeX]{.term
lem=BibTeX
def="A program designed to format bibliographical entries"
} citations. Note that in LaTeX environments,
the material between the begin and end tags will be interpreted
as [raw LaTeX]{.term
lem="Raw code"
def="Code inserted **untouched** in the output."
}, not as Markdown.
(From Pandoc manual)

View File

@ -0,0 +1,245 @@
FORMAT = 'latex'
-- Configuration
local metadata_test = [[
---
mrgnn-define-renderings:
- class: term
body-text: '`**§content§**`'
note-text: '`§attr.lem§: *§attr.def§*`'
- class: warning
body-text: ''
note-text: '`**§content§**`'
- class: custom-csname
body-text: ''
note-text: '`§content§`'
csname: mrgnnCustom
---
]]
local meta = pandoc.read(metadata_test).meta
get_renderings_config(meta)
assert(config.term.body_text == '**§content§**')
assert(config.term.note_text == '§attr.lem§: *§attr.def§*')
assert(not config.term.docx_body_text)
assert(not config.term.docx_note_text)
assert(config.term.odt_body_text == '**§content§**')
assert(config.term.odt_note_text == '§attr.lem§: *§attr.def§*')
assert(not config.warning.body_text)
assert(config.warning.note_text == '**§content§**')
assert(not config.warning.docx_body_text)
assert(not config.warning.docx_note_text)
assert(not config.warning.odt_body_text)
assert(config.warning.odt_note_text == '**§content§**')
-- Creation of marginal notes
local span_simple = pandoc.Span(pandoc.Str('test'))
local span_with_spaces = pandoc.Span({ pandoc.Str('another'),
pandoc.Space(), pandoc.Str('with'),
pandoc.Space(), pandoc.Str('spaces') })
local span_definition = pandoc.Span(pandoc.Str('whales'),
{ lem = 'whale', def = 'an animal' })
local span_paragraph = pandoc.Span(pandoc.Str('test'), { par = 2 })
local span_hyperlink = pandoc.Span(
{ pandoc.Str('a'), pandoc.Space(), pandoc.Str('description') },
{ target = 'www.example.org' })
local span_hyperlink_bis = pandoc.Span(
{ pandoc.Str('My'), pandoc.Space(),
pandoc.Str('other'), pandoc.Space(), pandoc.Str('link') },
{ target = 'www.example.net' })
local span_page_range = pandoc.Span(pandoc.Str('test'), { begin = 3, ['end'] = 5 })
local pandoc_fragment = template_to_pandoc_fragment('Fixed value')
assert(#pandoc_fragment == 3)
assert(pandoc_fragment[1].t == 'Str')
assert(pandoc_fragment[1].text == 'Fixed')
assert(pandoc_fragment[2].t == 'Space')
assert(pandoc_fragment[3].t == 'Str')
assert(pandoc_fragment[3].text == 'value')
local placeholders_paths = get_paths_to_placeholders(pandoc_fragment)
assert(#placeholders_paths == 0)
local filled = replace_placeholders(pandoc_fragment, placeholders_paths,
span_simple.content, span_simple.attributes)
assert(#filled == 3)
assert(filled[1].t == 'Str')
assert(filled[1].text == 'Fixed')
assert(filled[2].t == 'Space')
assert(filled[3].t == 'Str')
assert(filled[3].text == 'value')
local pandoc_fragment = template_to_pandoc_fragment('§content§')
assert(pandoc_fragment[1].t == 'Str')
assert(pandoc_fragment[1].text ==
PLACEHOLDER_BEGIN .. PLACEHOLDER_LABEL ..
'content' ..
PLACEHOLDER_LABEL .. PLACEHOLDER_END)
local placeholders_paths = get_paths_to_placeholders(pandoc_fragment)
assert(#placeholders_paths == 1)
assert(#placeholders_paths[1] == 2)
assert(placeholders_paths[1][2] == 'text')
local filled = replace_placeholders(pandoc_fragment, placeholders_paths,
span_simple.content, span_simple.attributes)
assert(#filled == 1)
assert(filled[1].t == 'Str')
assert(filled[1].text == span_simple.content[1].text)
local pandoc_fragment = template_to_pandoc_fragment('§content§')
local filled = replace_placeholders(pandoc_fragment, placeholders_paths,
span_with_spaces.content, span_with_spaces.attributes)
assert(#filled == 5)
assert(filled[1].t == 'Str')
assert(filled[1].text == span_with_spaces.content[1].text)
assert(filled[5].t == 'Str')
assert(filled[5].text == span_with_spaces.content[5].text)
local pandoc_fragment = template_to_pandoc_fragment('**§content§**')
assert(pandoc_fragment[1].t == 'Strong')
assert(pandoc_fragment[1].content[1].t == 'Str')
local placeholders_paths = get_paths_to_placeholders(pandoc_fragment)
assert(#placeholders_paths == 1)
assert(#placeholders_paths[1] == 4)
assert(placeholders_paths[1][1] == 1)
assert(placeholders_paths[1][2] == 'content')
assert(placeholders_paths[1][3] == 1)
assert(placeholders_paths[1][4] == 'text')
local filled = replace_placeholders(pandoc_fragment, placeholders_paths,
span_simple.content, span_simple.attributes)
assert(#filled == 1)
assert(filled[1].t == 'Strong')
assert(#filled[1].content == 1)
assert(filled[1].content[1].t == 'Str')
assert(filled[1].content[1].text == 'test')
local pandoc_fragment = template_to_pandoc_fragment('§attr.lem§: *§attr.def§.*')
assert(pandoc_fragment[1].t == 'Str')
assert(pandoc_fragment[1].text ==
PLACEHOLDER_BEGIN .. PLACEHOLDER_LABEL ..
'attr.lem' ..
PLACEHOLDER_LABEL .. PLACEHOLDER_END..
':')
assert(pandoc_fragment[3].t == 'Emph')
assert(pandoc_fragment[3].content[1].t == 'Str')
assert(pandoc_fragment[3].content[1].text ==
PLACEHOLDER_BEGIN .. PLACEHOLDER_LABEL ..
'attr.def' ..
PLACEHOLDER_LABEL .. PLACEHOLDER_END..
'.')
local placeholders_paths = get_paths_to_placeholders(pandoc_fragment)
assert(#placeholders_paths == 2)
assert(#placeholders_paths[1] == 2)
assert(placeholders_paths[1][1] == 1)
assert(#placeholders_paths[2] == 4)
assert(placeholders_paths[2][1] == 3)
assert(placeholders_paths[2][2] == 'content')
assert(placeholders_paths[2][3] == 1)
assert(placeholders_paths[2][4] == 'text')
local filled = replace_placeholders(pandoc_fragment, placeholders_paths,
span_definition.content, span_definition.attributes)
assert(#filled == 3)
assert(filled[1].t == 'Str')
assert(filled[1].text == 'whale:')
assert(filled[3].t == 'Emph')
assert(#filled[3].content == 3)
assert(filled[3].content[1].t == 'Str')
assert(filled[3].content[1].text == 'an')
-- If some keys are not present,
-- the placeholders are replaced with bold UNDEFINED
-- and a warning is issued.
local pandoc_fragment = template_to_pandoc_fragment('§attr.lem§: *§attr.def§.*')
local filled = replace_placeholders(pandoc_fragment, placeholders_paths,
span_simple.content, span_simple.attributes)
assert(#filled == 4)
assert(filled[1].t == 'Strong')
assert(filled[1].content[1].text == UNDEFINED)
assert(filled[4].t == 'Emph')
assert(#filled[4].content == 2)
assert(filled[4].content[1].t == 'Strong')
assert(filled[4].content[1].content[1].text == UNDEFINED)
local pandoc_fragment = template_to_pandoc_fragment('See \\§ §attr.par§')
assert(pandoc_fragment[2].t == 'Space')
assert(pandoc_fragment[3].t == 'Str')
assert(pandoc_fragment[3].text ==
'§ ' ..
PLACEHOLDER_BEGIN .. PLACEHOLDER_LABEL ..
'attr.par' ..
PLACEHOLDER_LABEL .. PLACEHOLDER_END)
local placeholders_paths = get_paths_to_placeholders(pandoc_fragment)
assert(#placeholders_paths == 1)
assert(#placeholders_paths[1] == 2)
assert(placeholders_paths[1][1] == 3)
local filled = replace_placeholders(pandoc_fragment, placeholders_paths,
span_paragraph.content, span_paragraph.attributes)
assert(#filled == 3)
assert(filled[3].t == 'Str')
assert(filled[3].text == '§ 2')
local pandoc_fragment = template_to_pandoc_fragment('[§content§](https://§attr.target§)')
assert(pandoc_fragment[1].target ==
'https://' ..
PLACEHOLDER_BEGIN .. PLACEHOLDER_LABEL ..
'attr.target' ..
PLACEHOLDER_LABEL .. PLACEHOLDER_END)
local placeholders_paths = get_paths_to_placeholders(pandoc_fragment)
assert(#placeholders_paths[2] == 2)
assert(placeholders_paths[2][1] == 1)
assert(placeholders_paths[2][2] == 'target')
local filled = replace_placeholders(pandoc_fragment, placeholders_paths,
span_hyperlink.content, span_hyperlink.attributes)
assert(#filled == 1)
assert(filled[1].t == 'Link')
assert(#filled[1].content == 3)
assert(filled[1].target == 'https://www.example.org')
local pandoc_fragment = template_to_pandoc_fragment('pp. §attr.begin§§attr.end§')
assert(#pandoc_fragment == 1)
assert(pandoc_fragment[1].t == 'Str')
local placeholders_paths = get_paths_to_placeholders(pandoc_fragment)
assert(#placeholders_paths == 1)
assert(#placeholders_paths[1] == 2)
local filled = replace_placeholders(pandoc_fragment, placeholders_paths,
span_page_range.content, span_page_range.attributes)
assert(#filled == 1)
assert(filled[1].t == 'Str')
assert(filled[1].text == 'pp. 35')
-- Top-level invocation
define_rendering_functions(meta)
local text_with_note = {}
local span_term_one = pandoc.Span(pandoc.Str('whales'),
{ class = 'term', lem = 'whale', def = 'an animal' })
local span_term_two = pandoc.Span(pandoc.Str('wand'),
{ class = 'term', lem = 'wand', def = 'a magical device' })
text_with_note = render_margin_notes(span_term_one)
assert(#text_with_note[1].content == 1)
assert(text_with_note[1].content[1].t == 'Strong')
assert(text_with_note[1].content[1].content[1].text == 'whales')
assert(text_with_note[2].t == 'RawInline')
assert(text_with_note[6].t == 'RawInline')
text_with_note = render_margin_notes(span_term_two)
assert(#text_with_note[1].content == 1)
assert(text_with_note[1].content[1].t == 'Strong')
assert(text_with_note[1].content[1].content[1].text == 'wand')
local span_warning_one = pandoc.Span(pandoc.Str('beware!'),
{ class = 'warning' })
text_with_note = render_margin_notes(span_warning_one)
assert(text_with_note[3].t == 'Strong')
assert(text_with_note[3].content[1].t == 'Str')
assert(text_with_note[3].content[1].text == 'beware!')
-- Custom control sequences for LaTeX and ConTeXt
local span_custom_csname = pandoc.Span(pandoc.Str('dummy'),
{ class = 'custom-csname' })
text_with_note = render_margin_notes(span_custom_csname)
assert(text_with_note[2].t == 'RawInline')
assert(text_with_note[2].text == '\\mrgnnCustom{')