columns.lua 27 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036
  1. --[[-- # Columns - multiple column support in Pandoc's markdown.
  2. This Lua filter provides support for multiple columns in
  3. latex and html outputs. For details, see README.md.
  4. @author Julien Dutant <julien.dutant@kcl.ac.uk>
  5. @copyright 2021 Julien Dutant
  6. @license MIT - see LICENSE file for details.
  7. @release 1.1
  8. ]]
  9. -- # Internal settings
  10. -- target_formats filter is triggered when those format are targeted
  11. local target_formats = {
  12. "html.*",
  13. "latex",
  14. }
  15. -- # Helper functions
  16. --- type: pandoc-friendly type function
  17. -- panbdoc.utils.type is only defined in Pandoc >= 2.17
  18. -- if it isn't, we extend Lua's type function to give the same values
  19. -- as pandoc.utils.type on Meta objects: Inlines, Inline, Blocks, Block,
  20. -- string and booleans
  21. -- Caution: not to be used on non-Meta Pandoc elements, the
  22. -- results will differ (only 'Block', 'Blocks', 'Inline', 'Inlines' in
  23. -- >=2.17, the .t string in <2.17).
  24. local utils = require('pandoc.utils')
  25. local type = utils.type or function (obj)
  26. local tag = type(obj) == 'table' and obj.t and obj.t:gsub('^Meta', '')
  27. return tag and tag ~= 'Map' and tag or type(obj)
  28. end
  29. --- Test whether the target format is in a given list.
  30. -- @param formats list of formats to be matched
  31. -- @return true if match, false otherwise
  32. local function format_matches(formats)
  33. for _,format in pairs(formats) do
  34. if FORMAT:match(format) then
  35. return true
  36. end
  37. end
  38. return false
  39. end
  40. --- Add a block to the document's header-includes meta-data field.
  41. -- @param meta the document's metadata block
  42. -- @param block Pandoc block element (e.g. RawBlock or Para) to be added to header-includes
  43. -- @return meta the modified metadata block
  44. local function add_header_includes(meta, block)
  45. local header_includes = pandoc.List:new()
  46. -- use meta['header-includes']
  47. if meta['header-includes'] then
  48. if type(meta['header-includes']) == 'List' then
  49. header_includes:extend(meta['header-includes'])
  50. else
  51. header_includes:insert(meta['header-includes'])
  52. end
  53. end
  54. -- insert `block` in header-includes
  55. header_includes:insert(pandoc.MetaBlocks({block}))
  56. -- save header-includes in the document's meta
  57. meta['header-includes'] = header_includes
  58. return meta
  59. end
  60. --- Add a class to an element.
  61. -- @param element Pandoc AST element
  62. -- @param class name of the class to be added (string)
  63. -- @return the modified element, or the unmodified element if the element has no classes
  64. local function add_class(element, class)
  65. -- act only if the element has classes
  66. if element.attr and element.attr.classes then
  67. -- if the class is absent, add it
  68. if not element.attr.classes:includes(class) then
  69. element.attr.classes:insert(class)
  70. end
  71. end
  72. return element
  73. end
  74. --- Removes a class from an element.
  75. -- @param element Pandoc AST element
  76. -- @param class name of the class to be removed (string)
  77. -- @return the modified element, or the unmodified element if the element has no classes
  78. local function remove_class(element, class)
  79. -- act only if the element has classes
  80. if element.attr and element.attr.classes then
  81. -- if the class is present, remove it
  82. if element.attr.classes:includes(class) then
  83. element.attr.classes = element.attr.classes:filter(
  84. function(x)
  85. return not (x == class)
  86. end
  87. )
  88. end
  89. end
  90. return element
  91. end
  92. --- Set the value of an element's attribute.
  93. -- @param element Pandoc AST element to be modified
  94. -- @param key name of the attribute to be set (string)
  95. -- @param value value to be set. If nil, the attribute is removed.
  96. -- @return the modified element, or the element if it's not an element with attributes.
  97. local function set_attribute(element,key,value)
  98. -- act only if the element has attributes
  99. if element.attr and element.attr.attributes then
  100. -- if `value` is `nil`, remove the attribute
  101. if value == nil then
  102. if element.attr.attributes[key] then
  103. element.attr.attributes[key] = nil
  104. end
  105. -- otherwise set its value
  106. else
  107. element.attr.attributes[key] = value
  108. end
  109. end
  110. return element
  111. end
  112. --- Add html style markup to an element's attributes.
  113. -- @param element the Pandoc AST element to be modified
  114. -- @param style the style markup to add (string in CSS)
  115. -- @return the modified element, or the unmodified element if it's an element without attributes
  116. local function add_to_html_style(element, style)
  117. -- act only if the element has attributes
  118. if element.attr and element.attr.attributes then
  119. -- if the element has style markup, append
  120. if element.attr.attributes['style'] then
  121. element.attr.attributes['style'] =
  122. element.attr.attributes['style'] .. '; ' .. style .. ' ;'
  123. -- otherwise create
  124. else
  125. element.attr.attributes['style'] = style .. ' ;'
  126. end
  127. end
  128. return element
  129. end
  130. --- Translate an English number name into a number.
  131. -- Converts cardinals ("one") and numerals ("first").
  132. -- Returns nil if the name isn't understood.
  133. -- @param name an English number name (string)
  134. -- @return number or nil
  135. local function number_by_name(name)
  136. local names = {
  137. one = 1,
  138. two = 2,
  139. three = 3,
  140. four = 4,
  141. five = 5,
  142. six = 6,
  143. seven = 7,
  144. eight = 8,
  145. nine = 9,
  146. ten = 10,
  147. first = 1,
  148. second = 2,
  149. third = 3,
  150. fourth = 4,
  151. fifth = 5,
  152. sixth = 6,
  153. seventh = 7,
  154. eighth = 8,
  155. ninth = 9,
  156. tenth = 10,
  157. }
  158. result = nil
  159. if name and names[name] then
  160. return names[name]
  161. end
  162. end
  163. --- Convert some CSS values (lengths, colous) to LaTeX equivalents.
  164. -- Example usage: `css_values_to_latex("1px solid black")` returns
  165. -- `{ length = "1pt", color = "black", colour = "black"}`.
  166. -- @param css_str a CSS string specifying a value
  167. -- @return table with keys `length`, `color` (alias `colour`) if found
  168. local function css_values_to_latex(css_str)
  169. -- color conversion table
  170. -- keys are CSS values, values are LaTeX equivalents
  171. latex_colors = {
  172. -- xcolor always available
  173. black = 'black',
  174. blue = 'blue',
  175. brown = 'brown',
  176. cyan = 'cyan',
  177. darkgray = 'darkgray',
  178. gray = 'gray',
  179. green = 'green',
  180. lightgray = 'lightgray',
  181. lime = 'lime',
  182. magenta = 'magenta',
  183. olive = 'olive',
  184. orange = 'orange',
  185. pink = 'pink',
  186. purple = 'purple',
  187. red = 'red',
  188. teal = 'teal',
  189. violet = 'violet',
  190. white = 'white',
  191. yellow = 'yellow',
  192. -- css1 colors
  193. silver = 'lightgray',
  194. fuschia = 'magenta',
  195. aqua = 'cyan',
  196. }
  197. local result = {}
  198. -- look for color values
  199. -- by color name
  200. -- rgb, etc.: to be added
  201. local color = ''
  202. -- space in front simplifies pattern matching
  203. css_str = ' ' .. css_str
  204. -- look for colour names
  205. for text in string.gmatch(css_str, '[%s](%a+)') do
  206. -- if we have LaTeX equivalent of `text`, store it
  207. if latex_colors[text] then
  208. result['color'] = latex_colors[text]
  209. end
  210. end
  211. -- provide British spelling
  212. if result['color'] then
  213. result['colour'] = result['color']
  214. end
  215. -- look for lengths
  216. -- 0 : converted to 0em
  217. if string.find(css_str, '%s0%s') then
  218. result['length'] = '0em'
  219. end
  220. -- px : converted to pt
  221. for text in string.gmatch(css_str, '(%s%d+)px') do
  222. result['length'] = text .. 'pt'
  223. end
  224. -- lengths units to be kept as is
  225. -- nb, % must be escaped
  226. -- nb, if several found, the latest type is preserved
  227. keep_units = { '%%', 'pt', 'mm', 'cm', 'in', 'ex', 'em' }
  228. for _,unit in pairs(keep_units) do
  229. -- .11em format
  230. for text in string.gmatch(css_str, '%s%.%d+'.. unit) do
  231. result['length'] = text
  232. end
  233. -- 2em and 1.2em format
  234. for text in string.gmatch(css_str, '%s%d+%.?%d*'.. unit) do
  235. result['length'] = text
  236. end
  237. end
  238. return result
  239. end
  240. --- Ensures that a string specifies a LaTeX length
  241. -- @param text text to be checked
  242. -- @return text if it is a LaTeX length, `nil` otherwise
  243. local function ensures_latex_length(text)
  244. -- LaTeX lengths units
  245. -- nb, % must be escaped in lua patterns
  246. units = { '%%', 'pt', 'mm', 'cm', 'in', 'ex', 'em' }
  247. local result = nil
  248. -- ignore spaces, controls and punctuation other than
  249. -- dot, plus, minus
  250. text = string.gsub(text, "[%s%c,;%(%)%[%]%*%?%%%^%$]+", "")
  251. for _,unit in pairs(units) do
  252. -- match .11em format and 1.2em format
  253. if string.match(text, '^%.%d+'.. unit .. '$') or
  254. string.match(text, '^%d+%.?%d*'.. unit .. '$') then
  255. result = text
  256. end
  257. end
  258. return result
  259. end
  260. -- # Filter-specific functions
  261. --- Process the metadata block.
  262. -- Adds any needed material to the document's metadata block.
  263. -- @param meta the document's metadata element
  264. local function process_meta(meta)
  265. -- in LaTeX, require the `multicols` package
  266. if FORMAT:match('latex') then
  267. return add_header_includes(meta,
  268. pandoc.RawBlock('latex', '\\usepackage{multicol}\n'))
  269. end
  270. -- in html, ensure that the first element of `columns` div
  271. -- has a top margin of zero (otherwise we get white space
  272. -- on the top of the first column)
  273. -- idem for the first element after a `column-span` element
  274. if FORMAT:match('html.*') then
  275. html_header = [[
  276. <style>
  277. /* Styles added by the columns.lua pandoc filter */
  278. .columns :first-child {margin-top: 0;}
  279. .column-span + * {margin-top: 0;}
  280. </style>
  281. ]]
  282. return add_header_includes(meta, pandoc.RawBlock('html', html_header))
  283. end
  284. return meta
  285. end
  286. --- Convert explicit columnbreaks.
  287. -- This function converts any explict columnbreak markup in an element
  288. -- into a single syntax: a Div with class `columnbreak`.
  289. -- Note: if there are `column` Divs in the element we keep them
  290. -- in case they harbour further formatting (e.g. html classes). However
  291. -- we remove their `column` class to avoid double-processing when
  292. -- column fields are nested.
  293. -- @param elem Pandoc native Div element
  294. -- @return elem modified as needed
  295. local function convert_explicit_columbreaks(elem)
  296. -- if `elem` ends with a `column` Div, this last Div should
  297. -- not generate a columnbreak. We tag it to make sure we don't convert it.
  298. if elem.content[#elem.content] and elem.content[#elem.content].classes
  299. and elem.content[#elem.content].classes:includes('column') then
  300. elem.content[#elem.content] =
  301. add_class(elem.content[#elem.content], 'column-div-in-last-position')
  302. end
  303. -- processes `column` Divs and `\columnbreak` LaTeX RawBlocks
  304. filter = {
  305. Div = function (el)
  306. -- syntactic sugar: `column-break` converted to `columnbreak`
  307. if el.classes:includes("column-break") then
  308. el = add_class(el,"columnbreak")
  309. el = remove_class(el,"column-break")
  310. end
  311. if el.classes:includes("column") then
  312. -- with `column` Div, add a break if it's not in last position
  313. if not el.classes:includes('column-div-in-last-position') then
  314. local breaking_div = pandoc.Div({})
  315. breaking_div = add_class(breaking_div, "columnbreak")
  316. el.content:insert(breaking_div)
  317. -- if it's in the last position, remove the custom tag
  318. else
  319. el = remove_class(el, 'column-div-in-last-position')
  320. end
  321. -- remove `column` classes, but leave the div and other
  322. -- attributes the user might have added
  323. el = remove_class(el, 'column')
  324. end
  325. return el
  326. end,
  327. RawBlock = function (el)
  328. if el.format == "tex" and el.text == '\\columnbreak' then
  329. local breaking_div = pandoc.Div({})
  330. breaking_div = add_class(breaking_div, "columnbreak")
  331. return breaking_div
  332. else
  333. return el
  334. end
  335. end
  336. }
  337. return pandoc.walk_block(elem, filter)
  338. end
  339. --- Tag an element with the number of explicit columnbreaks it contains.
  340. -- Counts the number of epxlicit columnbreaks contained in an element and
  341. -- tags the element with a `number_explicit_columnbreaks` attribute.
  342. -- In the process columnbreaks are tagged with the class `columnbreak_already_counted`
  343. -- in order to avoid double-counting when multi-columns are nested.
  344. -- @param elem Pandoc element (native Div element of class `columns`)
  345. -- @return elem with the attribute `number_explicit_columnbreaks` set.
  346. local function tag_with_number_of_explicit_columnbreaks(elem)
  347. local number_columnbreaks = 0
  348. local filter = {
  349. Div = function(el)
  350. if el.classes:includes('columnbreak') and
  351. not el.classes:includes('columnbreak_already_counted') then
  352. number_columnbreaks = number_columnbreaks + 1
  353. el = add_class(el, 'columnbreak_already_counted')
  354. end
  355. return el
  356. end
  357. }
  358. elem = pandoc.walk_block(elem, filter)
  359. elem = set_attribute(elem, 'number_explicit_columnbreaks',
  360. number_columnbreaks)
  361. return elem
  362. end
  363. --- Consolidate aliases for column attributes.
  364. -- Provides syntacic sugar: unifies various ways of
  365. -- specifying attributes of a multi-column environment.
  366. -- When several specifications conflit, favours `column-gap` and
  367. -- `column-rule` specifications.
  368. -- @param elem Pandoc element (Div of class `columns`) with column attributes.
  369. -- @return elem modified as needed.
  370. local function consolidate_colattrib_aliases(elem)
  371. if elem.attr and elem.attr.attributes then
  372. -- `column-gap` if the preferred syntax is set, erase others
  373. if elem.attr.attributes["column-gap"] then
  374. elem = set_attribute(elem, "columngap", nil)
  375. elem = set_attribute(elem, "column-sep", nil)
  376. elem = set_attribute(elem, "columnsep", nil)
  377. -- otherwise fetch and unset any alias
  378. else
  379. if elem.attr.attributes["columnsep"] then
  380. elem = set_attribute(elem, "column-gap",
  381. elem.attr.attributes["columnsep"])
  382. elem = set_attribute(elem, "columnsep", nil)
  383. end
  384. if elem.attr.attributes["column-sep"] then
  385. elem = set_attribute(elem, "column-gap",
  386. elem.attr.attributes["column-sep"])
  387. elem = set_attribute(elem, "column-sep", nil)
  388. end
  389. if elem.attr.attributes["columngap"] then
  390. elem = set_attribute(elem, "column-gap",
  391. elem.attr.attributes["columngap"])
  392. elem = set_attribute(elem, "columngap", nil)
  393. end
  394. end
  395. -- `column-rule` if the preferred syntax is set, erase others
  396. if elem.attr.attributes["column-rule"] then
  397. elem = set_attribute(elem, "columnrule", nil)
  398. -- otherwise fetch and unset any alias
  399. else
  400. if elem.attr.attributes["columnrule"] then
  401. elem = set_attribute(elem, "column-rule",
  402. elem.attr.attributes["columnrule"])
  403. elem = set_attribute(elem, "columnrule", nil)
  404. end
  405. end
  406. end
  407. return elem
  408. end
  409. --- Pre-process a Div of class `columns`.
  410. -- Converts explicit column breaks into a unified syntax
  411. -- and count the Div's number of columns.
  412. -- When several columns are nested Pandoc will apply
  413. -- this filter to the innermost `columns` Div first;
  414. -- we use that feature to prevent double-counting.
  415. -- @param elem Pandoc element to be processes (Div of class `columns`)
  416. -- @return elem modified as needed
  417. local function preprocess_columns(elem)
  418. -- convert any explicit column syntax in a single format:
  419. -- native Divs with class `columnbreak`
  420. elem = convert_explicit_columbreaks(elem)
  421. -- count explicit columnbreaks
  422. elem = tag_with_number_of_explicit_columnbreaks(elem)
  423. return elem
  424. end
  425. --- Determine the number of column in a `columns` Div.
  426. -- Looks up two attributes in the Div: the user-specified
  427. -- `columns-count` and the filter-generated `number_explicit_columnbreaks`
  428. -- which is based on the number of explicit breaks specified.
  429. -- The final number of columns will be 2 or whichever of `column-count` and
  430. -- `number_explicit_columnbreaks` is the highest. This ensures there are
  431. -- enough columns for all explicit columnbreaks.
  432. -- This provides a single-column when the user specifies `column-count = 1` and
  433. -- there are no explicit columnbreaks.
  434. -- @param elem Pandoc element (Div of class `columns`) whose number of columns is to be determined.
  435. -- @return number of columns (number, default 2).
  436. local function determine_column_count(elem)
  437. -- is there a specified column count?
  438. local specified_column_count = 0
  439. if elem.attr.attributes and elem.attr.attributes['column-count'] then
  440. specified_column_count = tonumber(
  441. elem.attr.attributes["column-count"])
  442. end
  443. -- is there an count of explicit columnbreaks?
  444. local number_explicit_columnbreaks = 0
  445. if elem.attr.attributes and elem.attr.attributes['number_explicit_columnbreaks'] then
  446. number_explicit_columnbreaks = tonumber(
  447. elem.attr.attributes['number_explicit_columnbreaks']
  448. )
  449. set_attribute(elem, 'number_explicit_columnbreaks', nil)
  450. end
  451. -- determines the number of columns
  452. -- default 2
  453. -- recall that number of columns = nb columnbreaks + 1
  454. local number_columns = 2
  455. if specified_column_count > 0 or number_explicit_columnbreaks > 0 then
  456. if (number_explicit_columnbreaks + 1) > specified_column_count then
  457. number_columns = number_explicit_columnbreaks + 1
  458. else
  459. number_columns = specified_column_count
  460. end
  461. end
  462. return number_columns
  463. end
  464. --- Convert a pandoc Header to a list of inlines for latex output.
  465. -- @param header Pandoc Header element
  466. -- @return list of Inline elements
  467. local function header_to_latex_and_inlines(header)
  468. -- @todo check if level interpretation has been shifted, e.g. section is level 2
  469. -- @todo we could check the Pandoc state to check whether hypertargets are required?
  470. local latex_header = {
  471. 'section',
  472. 'subsection',
  473. 'subsubsection',
  474. 'paragraph',
  475. 'subparagraph',
  476. }
  477. -- create a list if the header's inlines
  478. local inlines = pandoc.List:new(header.content)
  479. -- wrap in a latex_header if available
  480. if header.level and latex_header[header.level] then
  481. inlines:insert(1, pandoc.RawInline('latex',
  482. '\\' .. latex_header[header.level] .. '{'))
  483. inlines:insert(pandoc.RawInline('latex', '}'))
  484. end
  485. -- wrap in a link if available
  486. if header.identifier then
  487. inlines:insert(1, pandoc.RawInline('latex',
  488. '\\hypertarget{' .. header.identifier .. '}{%\n'))
  489. inlines:insert(pandoc.RawInline('latex',
  490. '\\label{' .. header.identifier .. '}}'))
  491. end
  492. return inlines
  493. end
  494. --- Format column span in LaTeX.
  495. -- Formats a bit of text spanning across all columns for LaTeX output.
  496. -- If the colspan is only one block, it is turned into an option
  497. -- of a new `multicol` environment. Otherwise insert it is
  498. -- inserted between the two `multicol` environments.
  499. -- @param elem Pandoc element that is supposed to span across all
  500. -- columns.
  501. -- @param number_columns number of columns in the present environment.
  502. -- @return a pandoc RawBlock element in LaTeX format
  503. local function format_colspan_latex(elem, number_columns)
  504. local result = pandoc.List:new()
  505. -- does the content consists of a single header?
  506. if #elem.content == 1 and elem.content[1].t == 'Header' then
  507. -- create a list of inlines
  508. inlines = pandoc.List:new()
  509. inlines:insert(pandoc.RawInline('latex',
  510. "\\end{multicols}\n"))
  511. inlines:insert(pandoc.RawInline('latex',
  512. "\\begin{multicols}{".. number_columns .."}["))
  513. inlines:extend(header_to_latex_and_inlines(elem.content[1]))
  514. inlines:insert(pandoc.RawInline('latex',"]\n"))
  515. -- insert as a Plain block
  516. result:insert(pandoc.Plain(inlines))
  517. return result
  518. else
  519. result:insert(pandoc.RawBlock('latex',
  520. "\\end{multicols}\n"))
  521. result:extend(elem.content)
  522. result:insert(pandoc.RawBlock('latex',
  523. "\\begin{multicols}{".. number_columns .."}"))
  524. return result
  525. end
  526. end
  527. --- Format columns for LaTeX output
  528. -- @param elem Pandoc element (Div of "columns" class) containing the
  529. -- columns to be formatted.
  530. -- @return elem with suitable RawBlocks in LaTeX added
  531. local function format_columns_latex(elem)
  532. -- make content into a List object
  533. pandoc.List:new(elem.content)
  534. -- how many columns?
  535. number_columns = determine_column_count(elem)
  536. -- set properties and insert LaTeX environment
  537. -- we wrap the entire environment in `{...}` to
  538. -- ensure properties (gap, rule) don't carry
  539. -- over to following columns
  540. local latex_begin = '{'
  541. local latex_end = '}'
  542. if elem.attr.attributes then
  543. if elem.attr.attributes["column-gap"] then
  544. local latex_value = ensures_latex_length(
  545. elem.attr.attributes["column-gap"])
  546. if latex_value then
  547. latex_begin = latex_begin ..
  548. "\\setlength{\\columnsep}{" .. latex_value .. "}\n"
  549. end
  550. -- remove the `column-gap` attribute
  551. elem = set_attribute(elem, "column-gap", nil)
  552. end
  553. if elem.attr.attributes["column-rule"] then
  554. -- converts CSS value string to LaTeX values
  555. local latex_values = css_values_to_latex(
  556. elem.attr.attributes["column-rule"])
  557. if latex_values["length"] then
  558. latex_begin = latex_begin ..
  559. "\\setlength{\\columnseprule}{" ..
  560. latex_values["length"] .. "}\n"
  561. end
  562. if latex_values["color"] then
  563. latex_begin = latex_begin ..
  564. "\\renewcommand{\\columnseprulecolor}{\\color{" ..
  565. latex_values["color"] .. "}}\n"
  566. end
  567. -- remove the `column-rule` attribute
  568. elem = set_attribute(elem, "column-rule", nil)
  569. end
  570. end
  571. latex_begin = latex_begin ..
  572. "\\begin{multicols}{" .. number_columns .. "}\n"
  573. latex_end = "\\end{multicols}\n" .. latex_end
  574. elem.content:insert(1, pandoc.RawBlock('latex', latex_begin))
  575. elem.content:insert(pandoc.RawBlock('latex', latex_end))
  576. -- process blocks contained in `elem`
  577. -- turn any explicit columnbreaks into LaTeX markup
  578. -- turn `column-span` Divs into LaTeX markup
  579. filter = {
  580. Div = function(el)
  581. if el.classes:includes("columnbreak") then
  582. return pandoc.RawBlock('latex', "\\columnbreak\n")
  583. end
  584. if el.classes:includes("column-span-to-be-processed") then
  585. return format_colspan_latex(el, number_columns)
  586. end
  587. end
  588. }
  589. elem = pandoc.walk_block(elem, filter)
  590. return elem
  591. end
  592. --- Formats columns for html output.
  593. -- Uses CSS3 style added to the elements themselves.
  594. -- @param elem Pandoc element (Div of `columns` style)
  595. -- @return elem with suitable html attributes
  596. local function format_columns_html(elem)
  597. -- how many columns?
  598. number_columns = determine_column_count(elem)
  599. -- add properties to the `columns` Div
  600. elem = add_to_html_style(elem, 'column-count: ' .. number_columns)
  601. elem = set_attribute(elem, 'column-count', nil)
  602. if elem.attr.attributes then
  603. if elem.attr.attributes["column-gap"] then
  604. elem = add_to_html_style(elem, 'column-gap: ' ..
  605. elem.attr.attributes["column-gap"])
  606. -- remove the `column-gap` attribute
  607. elem = set_attribute(elem, "column-gap")
  608. end
  609. if elem.attr.attributes["column-rule"] then
  610. elem = add_to_html_style(elem, 'column-rule: ' ..
  611. elem.attr.attributes["column-rule"])
  612. -- remove the `column-rule` attribute
  613. elem = set_attribute(elem, "column-rule", nil)
  614. end
  615. end
  616. -- convert any explicit columnbreaks in CSS markup
  617. filter = {
  618. Div = function(el)
  619. -- format column-breaks
  620. if el.classes:includes("columnbreak") then
  621. el = add_to_html_style(el, 'break-after: column')
  622. -- remove columbreaks class to avoid double processing
  623. -- when nested
  624. -- clean up already-counted tag
  625. el = remove_class(el, "columnbreak")
  626. el = remove_class(el, "columnbreak_already_counted")
  627. -- format column-spans
  628. elseif el.classes:includes("column-span-to-be-processed") then
  629. el = add_to_html_style(el, 'column-span: all')
  630. -- remove column-span-to-be-processed class to avoid double processing
  631. -- add column-span class to allow for styling
  632. el = add_class(el, "column-span")
  633. el = remove_class(el, "column-span-to-be-processed")
  634. end
  635. return el
  636. end
  637. }
  638. elem = pandoc.walk_block(elem, filter)
  639. return elem
  640. end
  641. -- # Main filters
  642. --- Formating filter.
  643. -- Applied last, converts prepared columns in target output formats
  644. -- @field Div looks for `columns` class
  645. format_filter = {
  646. Div = function (element)
  647. -- pick up `columns` Divs for formatting
  648. if element.classes:includes ("columns") then
  649. if FORMAT:match('latex') then
  650. element = format_columns_latex(element)
  651. elseif FORMAT:match('html.*') then
  652. element = format_columns_html(element)
  653. end
  654. return element
  655. end
  656. end
  657. }
  658. --- Preprocessing filter.
  659. -- Processes meta-data fields and walks the document to pre-process
  660. -- columns blocks. Determine how many columns they contain, tags the
  661. -- last column Div, etc. Avoids double-counting when columns environments
  662. -- are nested.
  663. -- @field Div looks for `columns` class
  664. -- @field Meta processes the metadata block
  665. preprocess_filter = {
  666. Div = function (element)
  667. -- send `columns` Divs to pre-processing
  668. if element.classes:includes("columns") then
  669. return preprocess_columns(element)
  670. end
  671. end,
  672. Meta = function (meta)
  673. return process_meta(meta)
  674. end
  675. }
  676. --- Syntactic sugar filter.
  677. -- Provides alternative ways of specifying columns properties.
  678. -- Kept separate from the pre-processing filter for clarity.
  679. -- @field Div looks for Div of classes `columns` (and related) and `column-span`
  680. syntactic_sugar_filter = {
  681. Div = function(element)
  682. -- convert "two-columns" into `columns` Divs
  683. for _,class in pairs(element.classes) do
  684. -- match xxxcolumns, xxx_columns, xxx-columns
  685. -- if xxx is the name of a number, make
  686. -- a `columns` div and set its `column-count` attribute
  687. local number = number_by_name(
  688. string.match(class,'(%a+)[_%-]?columns$')
  689. )
  690. if number then
  691. element = set_attribute(element,
  692. "column-count", tostring(number))
  693. element = remove_class(element, class)
  694. element = add_class(element, "columns")
  695. end
  696. end
  697. -- allows different ways of specifying `columns` attributes
  698. if element.classes:includes('columns') then
  699. element = consolidate_colattrib_aliases(element)
  700. end
  701. -- `column-span` syntax
  702. -- mark up as "to-be-processed" to avoid
  703. -- double processing when nested
  704. if element.classes:includes('column-span') or
  705. element.classes:includes('columnspan') then
  706. element = add_class(element, 'column-span-to-be-processed')
  707. element = remove_class(element, 'column-span')
  708. element = remove_class(element, 'columnspan')
  709. end
  710. return element
  711. end
  712. }
  713. -- Main statement returns filters only if the
  714. -- target format matches our list. The filters
  715. -- returned are applied in the following order:
  716. -- 1. `syntatic_sugar_filter` deals with multiple syntax
  717. -- 2. `preprocessing_filter` converts all explicit
  718. -- columnbreaks into a common syntax and tags
  719. -- those that are already counted. We must do
  720. -- that for all `columns` environments before
  721. -- turning any break back into LaTeX `\columnbreak` blocks
  722. -- otherwise we mess up the count in nested `columns` Divs.
  723. -- 3. `format_filter` formats the columns after the counting
  724. -- has been done
  725. if format_matches(target_formats) then
  726. return {syntactic_sugar_filter,
  727. preprocess_filter,
  728. format_filter}
  729. else
  730. return
  731. end