diff --git a/core/src/main/kotlin/eu/iamgio/quarkdown/ast/base/block/Table.kt b/core/src/main/kotlin/eu/iamgio/quarkdown/ast/base/block/Table.kt index ba231b9d..12a2f9f7 100644 --- a/core/src/main/kotlin/eu/iamgio/quarkdown/ast/base/block/Table.kt +++ b/core/src/main/kotlin/eu/iamgio/quarkdown/ast/base/block/Table.kt @@ -13,10 +13,11 @@ import eu.iamgio.quarkdown.visitor.node.NodeVisitor * A table, consisting of columns, each of which has a header and multiple cells. * A table is location-trackable since, if requested by the user, it may show a caption displaying its location-based label. * @param columns columns of the table. Each column has a header and multiple cells + * @param caption optional caption of the table (Quarkdown extension) */ class Table( val columns: List, - override val caption: String? = "Test caption" /*null*/, + override val caption: String? = null, ) : NestableNode, LocationTrackableNode, CaptionableNode { // Exposing all the cell contents as this table's direct children // allows visiting them during a tree traversal. diff --git a/core/src/main/kotlin/eu/iamgio/quarkdown/ast/quarkdown/block/ImageFigure.kt b/core/src/main/kotlin/eu/iamgio/quarkdown/ast/quarkdown/block/ImageFigure.kt index 1af49dae..8bc146c9 100644 --- a/core/src/main/kotlin/eu/iamgio/quarkdown/ast/quarkdown/block/ImageFigure.kt +++ b/core/src/main/kotlin/eu/iamgio/quarkdown/ast/quarkdown/block/ImageFigure.kt @@ -17,13 +17,6 @@ class ImageFigure(val image: Image) : NestableNode, LocationTrackableNode, Capti */ override val caption: String? = image.link.title - /** - * Whether the figure is numerable and labelable. - * This is true if the figure has a [caption]. - */ - val isLabelable: Boolean - get() = caption != null - /** * A singleton list containing [image]. * This is needed to allow the image to be traversed by a tree iterator diff --git a/core/src/main/kotlin/eu/iamgio/quarkdown/context/hooks/LocationAwareLabelStorerHook.kt b/core/src/main/kotlin/eu/iamgio/quarkdown/context/hooks/LocationAwareLabelStorerHook.kt index 433728c6..e7d6d0ca 100644 --- a/core/src/main/kotlin/eu/iamgio/quarkdown/context/hooks/LocationAwareLabelStorerHook.kt +++ b/core/src/main/kotlin/eu/iamgio/quarkdown/context/hooks/LocationAwareLabelStorerHook.kt @@ -43,8 +43,8 @@ import eu.iamgio.quarkdown.document.numbering.NumberingFormat */ class LocationAwareLabelStorerHook(private val context: MutableContext) : AstIteratorHook { override fun attach(iterator: ObservableAstIterator) { - updateLabels(DocumentNumbering::figures, iterator, filter = { it.isLabelable }) - updateLabels(DocumentNumbering::tables, iterator) + updateLabels(DocumentNumbering::figures, iterator, filter = { it.caption != null }) + updateLabels
(DocumentNumbering::tables, iterator, filter = { it.caption != null }) } /** @@ -63,7 +63,7 @@ class LocationAwareLabelStorerHook(private val context: MutableContext) : AstIte // Gets the needed numbering format from the global numbering settings. val format = formatSupplier(context.documentInfo.numberingOrDefault ?: return) - if (format == null) return + if (format == null || format.isNonCounting) return // Stores the number of elements encountered at each location. val countAtLocation = mutableMapOf() diff --git a/core/src/main/kotlin/eu/iamgio/quarkdown/document/numbering/NumberingFormat.kt b/core/src/main/kotlin/eu/iamgio/quarkdown/document/numbering/NumberingFormat.kt index 6235d868..fdf9dbaa 100644 --- a/core/src/main/kotlin/eu/iamgio/quarkdown/document/numbering/NumberingFormat.kt +++ b/core/src/main/kotlin/eu/iamgio/quarkdown/document/numbering/NumberingFormat.kt @@ -46,6 +46,12 @@ data class NumberingFormat( val accuracy: Int get() = counterSymbolCount + /** + * Whether the format does not contain any counting symbols. + */ + val isNonCounting: Boolean + get() = counterSymbolCount <= 0 + /** * Converts the numbering format into a string. * For example, the [NumberingFormat] `1.A.a` would format the levels `1, 1, 0` as `2.B.a`. diff --git a/core/src/main/kotlin/eu/iamgio/quarkdown/lexer/patterns/BaseMarkdownInlineTokenRegexPatterns.kt b/core/src/main/kotlin/eu/iamgio/quarkdown/lexer/patterns/BaseMarkdownInlineTokenRegexPatterns.kt index ee97cbb8..09cbb5a4 100644 --- a/core/src/main/kotlin/eu/iamgio/quarkdown/lexer/patterns/BaseMarkdownInlineTokenRegexPatterns.kt +++ b/core/src/main/kotlin/eu/iamgio/quarkdown/lexer/patterns/BaseMarkdownInlineTokenRegexPatterns.kt @@ -107,10 +107,7 @@ open class BaseMarkdownInlineTokenRegexPatterns { RegexBuilder("\\[(label)\\]\\(\\s*(href)(?:\\s+(title))?\\s*\\)") .withReference("label", LABEL_HELPER) .withReference("href", "<(?:\\\\.|[^\\n<>\\\\])+>|[^\\s\\x00-\\x1f]*") - .withReference( - "title", - "\"(?:\\\\\"?|[^\"\\\\])*\"|'(?:\\\\'?|[^'\\\\])*'|\\((?:\\\\\\)?|[^)\\\\])*\\)", - ) + .withReference("title", DELIMITED_TITLE_HELPER) .build(), ) @@ -331,6 +328,10 @@ private const val LABEL_HELPER = "(?:\\[(?:\\\\.|[^\\[\\]\\\\])*\\]|\\\\.|`[^`]* private const val BLOCK_LABEL_HELPER = "(?!\\s*\\])(?:\\\\.|[^\\[\\]\\\\])+" +// "This is a title", 'This is a title', (This is a title) +internal const val DELIMITED_TITLE_HELPER = + "\"(?:\\\\\"?|[^\"\\\\])*\"|'(?:\\\\'?|[^'\\\\])*'|\\((?:\\\\\\)?|[^)\\\\])*\\)" + private const val COMMENT_TAG_HELPER = "comment" + "|^" + // self-closing tag diff --git a/core/src/main/kotlin/eu/iamgio/quarkdown/parser/BlockTokenParser.kt b/core/src/main/kotlin/eu/iamgio/quarkdown/parser/BlockTokenParser.kt index 41bfe819..0b10e669 100644 --- a/core/src/main/kotlin/eu/iamgio/quarkdown/parser/BlockTokenParser.kt +++ b/core/src/main/kotlin/eu/iamgio/quarkdown/parser/BlockTokenParser.kt @@ -31,6 +31,7 @@ import eu.iamgio.quarkdown.function.value.factory.ValueFactory import eu.iamgio.quarkdown.lexer.Lexer import eu.iamgio.quarkdown.lexer.Token import eu.iamgio.quarkdown.lexer.acceptAll +import eu.iamgio.quarkdown.lexer.patterns.DELIMITED_TITLE_HELPER import eu.iamgio.quarkdown.lexer.tokens.BlockCodeToken import eu.iamgio.quarkdown.lexer.tokens.BlockQuoteToken import eu.iamgio.quarkdown.lexer.tokens.BlockTextToken @@ -328,9 +329,24 @@ class BlockTokenParser(private val context: MutableContext) : BlockTokenVisitor< } } + // Quarkdown extension: a table may have a caption. + // A caption is located at the end of the table, after a line break, + // wrapped by a delimiter, the same way as a link/image title. + // "This is a caption", 'This is a caption', (This is a caption) + val captionRegex = Regex("^\\s*($DELIMITED_TITLE_HELPER)\\s*$") + // The found caption of the table, if any. + var caption: String? = null + // Other rows. groups.next().lineSequence() .filterNot { it.isBlank() } + .onEach { row -> + // Extract the caption if this is the caption row. + captionRegex.find(row)?.let { captionMatch -> + caption = captionMatch.groupValues.getOrNull(1)?.trimDelimiters() + } + } + .filterNot { caption != null } // The caption row is at the end of the table and not part of the table itself. .forEach { row -> var cellCount = 0 // Push cell. @@ -346,6 +362,7 @@ class BlockTokenParser(private val context: MutableContext) : BlockTokenVisitor< return Table( columns = columns.map { Table.Column(it.alignment, it.header, it.cells) }, + caption, ) } diff --git a/core/src/main/kotlin/eu/iamgio/quarkdown/rendering/html/QuarkdownHtmlNodeRenderer.kt b/core/src/main/kotlin/eu/iamgio/quarkdown/rendering/html/QuarkdownHtmlNodeRenderer.kt index 2932c1de..acc5477e 100644 --- a/core/src/main/kotlin/eu/iamgio/quarkdown/rendering/html/QuarkdownHtmlNodeRenderer.kt +++ b/core/src/main/kotlin/eu/iamgio/quarkdown/rendering/html/QuarkdownHtmlNodeRenderer.kt @@ -112,7 +112,7 @@ class QuarkdownHtmlNodeRenderer(context: Context) : BaseHtmlNodeRenderer(context node.caption?.let { caption -> +buildTag(captionTagName) { - +caption + +escapeCriticalContent(caption) // The label is set as an attribute for styling. optionalAttribute("data-element-label", label) // Localized name of the element (e.g. `figure` -> `Figure` for English locale). @@ -413,6 +413,7 @@ class QuarkdownHtmlNodeRenderer(context: Context) : BaseHtmlNodeRenderer(context } } + // Quarkdown introduces table captions, also numerated. override fun visit(node: Table) = super.tableBuilder(node).apply { numberedCaption(node, "caption", kindLocalizationKey = "table") diff --git a/core/src/test/kotlin/eu/iamgio/quarkdown/BlockParserTest.kt b/core/src/test/kotlin/eu/iamgio/quarkdown/BlockParserTest.kt index a6efd7e0..9d208503 100644 --- a/core/src/test/kotlin/eu/iamgio/quarkdown/BlockParserTest.kt +++ b/core/src/test/kotlin/eu/iamgio/quarkdown/BlockParserTest.kt @@ -470,6 +470,28 @@ class BlockParserTest { assertFalse(hasNext()) } + repeat(2) { + with(nodes.next()) { + assertEquals("Table caption", caption) + + val columns = columns.iterator() + with(columns.next()) { + assertEquals(Table.Alignment.NONE, alignment) + assertEquals(2, cells.size) + assertNodeEquals(Text("G H I"), cells[0].text.first()) + assertNodeEquals(Text("M N O"), cells[1].text.first()) + } + with(columns.next()) { + assertEquals(Table.Alignment.NONE, alignment) + assertEquals(2, cells.size) + assertNodeEquals(Text("J K L"), cells[0].text.first()) + assertNodeEquals(Text("P Q R"), cells[1].text.first()) + } + + assertFalse(columns.hasNext()) + } + } + assertFalse(nodes.hasNext()) } diff --git a/core/src/test/kotlin/eu/iamgio/quarkdown/HtmlNodeRendererTest.kt b/core/src/test/kotlin/eu/iamgio/quarkdown/HtmlNodeRendererTest.kt index c81c5b6b..67838840 100644 --- a/core/src/test/kotlin/eu/iamgio/quarkdown/HtmlNodeRendererTest.kt +++ b/core/src/test/kotlin/eu/iamgio/quarkdown/HtmlNodeRendererTest.kt @@ -676,6 +676,33 @@ class HtmlNodeRendererTest { ), ).render(), ) + + assertEquals( + out.next(), + Table( + listOf( + Table.Column( + Table.Alignment.NONE, + header = Table.Cell(listOf(Text("A"))), + cells = + listOf( + Table.Cell(listOf(Text("C"))), + Table.Cell(listOf(Text("E"))), + ), + ), + Table.Column( + Table.Alignment.NONE, + header = Table.Cell(listOf(Text("B"))), + cells = + listOf( + Table.Cell(listOf(Text("D"))), + Table.Cell(listOf(Text("F"))), + ), + ), + ), + caption = "Table 'caption'.", + ).render(), + ) } // Quarkdown diff --git a/core/src/test/resources/parsing/table.md b/core/src/test/resources/parsing/table.md index 6770978e..2b460b01 100644 --- a/core/src/test/resources/parsing/table.md +++ b/core/src/test/resources/parsing/table.md @@ -20,4 +20,17 @@ bar | baz | | A | B | |:------|----|---:| | **C** | AC | BC | -| **D** | AD | BD | \ No newline at end of file +| **D** | AD | BD | + +| A B C | D E F | +|-------|-------| +| G H I | J K L | +| M N O | P Q R | +"Table caption" + +| A B C | D E F | +|-------|-------| +| G H I | J K L | +| M N O | P Q R | +"Table caption" +| S T U | V W X | \ No newline at end of file diff --git a/core/src/test/resources/rendering/block/table.html b/core/src/test/resources/rendering/block/table.html index a965e5c2..f2ab6502 100644 --- a/core/src/test/resources/rendering/block/table.html +++ b/core/src/test/resources/rendering/block/table.html @@ -62,4 +62,40 @@ +
+ +--- + + + + + + + + + + + + + + + + + + +
+ A + + B +
+ C + + D +
+ E + + F +
+ Table 'caption'. +
\ No newline at end of file diff --git a/test/src/test/kotlin/eu/iamgio/quarkdown/test/FullPipelineTest.kt b/test/src/test/kotlin/eu/iamgio/quarkdown/test/FullPipelineTest.kt index 930c4d06..5c7b5f43 100644 --- a/test/src/test/kotlin/eu/iamgio/quarkdown/test/FullPipelineTest.kt +++ b/test/src/test/kotlin/eu/iamgio/quarkdown/test/FullPipelineTest.kt @@ -492,6 +492,23 @@ class FullPipelineTest { it, ) } + + execute( + """ + | Header 1 | Header 2 | Header 3 | + |----------|:--------:|----------| + | Cell 1 | Cell 2 | Cell 3 | + 'Table caption' + """.trimIndent(), + ) { + assertEquals( + "" + + "" + + "" + + "
Header 1Header 2Header 3
Cell 1Cell 2Cell 3
Table caption
", + it, + ) + } } @Test @@ -1111,13 +1128,15 @@ class FullPipelineTest { # A ## A/1 # B + ![](img.png '') """.trimIndent(), DEFAULT_OPTIONS.copy(enableLocationAwareness = true), ) { assertEquals( "

A

" + "

A/1

" + - "

B

", + "

B

" + + "
\"\"
", it, ) } @@ -1282,13 +1301,23 @@ class FullPipelineTest { # A ## A/1 # B + + ![](img.png "Caption") + + | A | B | C | + |---|---|---| + | D | E | F | + '' """.trimIndent(), DEFAULT_OPTIONS.copy(enableLocationAwareness = true), ) { assertEquals( "

A

" + "

A/1

" + - "

B

", + "

B

" + + "
\"\"
Caption
" + + "" + + "
ABC
DEF
", it, ) } @@ -1306,7 +1335,7 @@ class FullPipelineTest { # B - ![](img.png "Caption 3") + ![](img.png "") """.trimIndent(), DEFAULT_OPTIONS.copy(enableLocationAwareness = true), ) { @@ -1319,8 +1348,8 @@ class FullPipelineTest { "
Caption 2
" + "" + "

B

" + - "
\"\"" + - "
Caption 3
" + + "
\"\"" + + "
" + "
", it, ) @@ -1329,7 +1358,7 @@ class FullPipelineTest { execute( """ .noautopagebreak - .numbering headings:{1} figures:{1.A.a} + .numbering headings:{1} figures:{1.A.a} tables:{1.A.a} ![](img.png "Caption") @@ -1337,6 +1366,11 @@ class FullPipelineTest { ![](img.png "Caption") + | A | B | C | + |---|---|---| + | D | E | F | + 'Table caption' + ## A/1 ![](img.png "Caption") @@ -1345,6 +1379,11 @@ class FullPipelineTest { ![](img.png "Caption") + | A | B | C | + |---|---|---| + | D | E | F | + '' + # B ![](img.png "Caption") @@ -1363,6 +1402,9 @@ class FullPipelineTest { "
\"\"" + "
Caption
" + "
" + + "" + + "" + + "
ABC
DEF
Table caption
" + "

A/1

" + "
\"\"" + "
Caption
" + @@ -1371,6 +1413,8 @@ class FullPipelineTest { "
\"\"" + "
Caption
" + "
" + + "" + + "
ABC
DEF
" + "

B

" + "
\"\"" + "
Caption
" + @@ -1382,6 +1426,74 @@ class FullPipelineTest { it, ) } + + // Non-captioned elements are not counted. + execute( + """ + .noautopagebreak + .numbering figures:{1.1} tables:{1.1} + + # A + + ![](img.png) + + | A | B | C | + |---|---|---| + | D | E | F | + + ![](img.png "Caption") + + | A | B | C | + |---|---|---| + | D | E | F | + 'Caption' + """.trimIndent(), + DEFAULT_OPTIONS.copy(enableLocationAwareness = true), + ) { + assertEquals( + "

A

" + + "
\"\"
" + + "" + + "
ABC
DEF
" + + "
\"\"" + + "
Caption
" + + "
" + + "" + + "" + + "
ABC
DEF
Caption
", + it, + ) + } + + // Localized kind names. + execute( + """ + .noautopagebreak + .doclang {italian} + .numbering headings:{none} figures:{1.1} tables:{1.a} + + # A + + ![](img.png "Caption") + + | A | B | C | + |---|---|---| + | D | E | F | + (Caption) + """.trimIndent(), + DEFAULT_OPTIONS.copy(enableLocationAwareness = true), + ) { + assertEquals( + "

A

" + + "
\"\"" + + "
Caption
" + + "
" + + "" + + "" + + "
ABC
DEF
Caption
", + it, + ) + } } @Test