diff --git a/margin-notes/Makefile b/margin-notes/Makefile new file mode 100644 index 0000000..67fcc04 --- /dev/null +++ b/margin-notes/Makefile @@ -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 + diff --git a/margin-notes/README.md b/margin-notes/README.md new file mode 100644 index 0000000..3961912 --- /dev/null +++ b/margin-notes/README.md @@ -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 ): + +``` 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`. + diff --git a/margin-notes/margin-notes.lua b/margin-notes/margin-notes.lua new file mode 100644 index 0000000..2bc6af2 --- /dev/null +++ b/margin-notes/margin-notes.lua @@ -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, + '' .. + '' .. + ''), + pandoc.Span( + pandoc.Str(DOCX_TOOLTIP_ANCHOR), + { ['custom-style'] = DOCX_TOOLTIP_ANCHOR_SYTLE }), + pandoc.RawInline( + FORMAT, + '') + } + elseif FORMAT == 'opendocument' then + insert_in_table(margin_note, 1, pandoc.RawInline(FORMAT, + '')) + insert_in_table(margin_note, pandoc.RawInline(FORMAT, + '')) + 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 } +} diff --git a/margin-notes/sample.md b/margin-notes/sample.md new file mode 100644 index 0000000..d718332 --- /dev/null +++ b/margin-notes/sample.md @@ -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) diff --git a/margin-notes/test/test-functions.lua b/margin-notes/test/test-functions.lua new file mode 100644 index 0000000..cc1bb8a --- /dev/null +++ b/margin-notes/test/test-functions.lua @@ -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. 3–5') + +-- 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{')