Skip to content

Commit a0aaef5

Browse files
committed
feat(stdlib): allow parsing inline Markdown in csv
1 parent 1425462 commit a0aaef5

File tree

9 files changed

+152
-28
lines changed

9 files changed

+152
-28
lines changed

quarkdown-core/src/main/kotlin/com/quarkdown/core/ast/base/block/Table.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,14 +58,14 @@ class Table(
5858
* A mutable [Table.Column] which can be built incrementally.
5959
*/
6060
data class MutableColumn(
61-
var alignment: Table.Alignment,
62-
val header: Table.Cell,
63-
val cells: MutableList<Table.Cell>,
61+
var alignment: Alignment,
62+
val header: Cell,
63+
val cells: MutableList<Cell>,
6464
) {
6565
/**
6666
* @return an immutable [Table.Column] with the current state of this mutable column
6767
*/
68-
fun toColumn(): Table.Column = Table.Column(alignment, header, cells.toList())
68+
fun toColumn(): Column = Column(alignment, header, cells.toList())
6969
}
7070

7171
/**

quarkdown-core/src/main/kotlin/com/quarkdown/core/ast/dsl/InlineAstBuilder.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import com.quarkdown.core.ast.base.inline.Image
77
import com.quarkdown.core.ast.base.inline.LineBreak
88
import com.quarkdown.core.ast.base.inline.Link
99
import com.quarkdown.core.ast.base.inline.Strong
10+
import com.quarkdown.core.ast.base.inline.StrongEmphasis
1011
import com.quarkdown.core.ast.base.inline.Text
1112
import com.quarkdown.core.ast.quarkdown.inline.InlineCollapse
1213
import com.quarkdown.core.ast.quarkdown.inline.TextTransform
@@ -28,6 +29,11 @@ class InlineAstBuilder : AstBuilder() {
2829
*/
2930
fun emphasis(block: InlineAstBuilder.() -> Unit) = +Emphasis(buildInline(block))
3031

32+
/**
33+
* @see StrongEmphasis
34+
*/
35+
fun strongEmphasis(block: InlineAstBuilder.() -> Unit) = +StrongEmphasis(buildInline(block))
36+
3137
/**
3238
* @see Text
3339
*/

quarkdown-core/src/testFixtures/kotlin/com/quarkdown/core/TestUtils.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.quarkdown.core
22

3+
import com.quarkdown.core.ast.AstRoot
4+
import com.quarkdown.core.ast.InlineContent
35
import com.quarkdown.core.ast.Node
46
import com.quarkdown.core.context.MutableContext
57
import com.quarkdown.core.lexer.Lexer
@@ -24,6 +26,16 @@ fun assertNodeEquals(
2426
.usingRecursiveComparison()
2527
.isEqualTo(expected)!!
2628

29+
/**
30+
* Asserts that the contents of two inline content nodes are equal.
31+
* @param expected expected node
32+
* @param actual actual node
33+
*/
34+
fun assertNodeEquals(
35+
expected: InlineContent,
36+
actual: InlineContent,
37+
) = assertNodeEquals(AstRoot(expected), AstRoot(actual))
38+
2739
/**
2840
* Reads the text content of a test resource.
2941
* @param path path to the test resource

quarkdown-stdlib/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ plugins {
44

55
dependencies {
66
testImplementation("org.jetbrains.kotlin:kotlin-test")
7+
testImplementation("org.assertj:assertj-core:3.27.6")
78
testImplementation(testFixtures(project(":quarkdown-core")))
89
implementation(project(":quarkdown-core"))
910
implementation("se.sawano.java:alphanumeric-comparator:2.0.0")

quarkdown-stdlib/src/main/kotlin/com/quarkdown/stdlib/Data.kt

Lines changed: 42 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
package com.quarkdown.stdlib
22

33
import com.github.doyaaaaaken.kotlincsv.dsl.csvReader
4+
import com.quarkdown.core.ast.InlineContent
45
import com.quarkdown.core.ast.base.block.Table
5-
import com.quarkdown.core.ast.base.inline.Text
6+
import com.quarkdown.core.ast.dsl.buildInline
67
import com.quarkdown.core.context.Context
78
import com.quarkdown.core.function.library.module.QuarkdownModule
89
import com.quarkdown.core.function.library.module.moduleOf
@@ -16,6 +17,7 @@ import com.quarkdown.core.function.value.StringValue
1617
import com.quarkdown.core.function.value.UnorderedCollectionValue
1718
import com.quarkdown.core.function.value.data.Range
1819
import com.quarkdown.core.function.value.data.subList
20+
import com.quarkdown.core.function.value.factory.ValueFactory
1921
import com.quarkdown.core.function.value.wrappedAsValue
2022
import com.quarkdown.core.util.normalizeLineSeparators
2123
import com.quarkdown.stdlib.internal.AlphanumericComparator
@@ -179,42 +181,63 @@ fun fileName(
179181
return StringValue(name)
180182
}
181183

184+
/**
185+
* Strategies to parse CSV cell content.
186+
* @param transform function that transforms the cell content string into parsed content
187+
*/
188+
enum class CsvParsingMode(
189+
val transform: (String, Context) -> InlineContent,
190+
) {
191+
/** Cell content is treated as plain text. */
192+
PLAIN({ text, _ -> buildInline { text(text) } }),
193+
194+
/** Cell content is treated as inline Quarkdown. */
195+
MARKDOWN({ text, context -> ValueFactory.inlineMarkdown(text, context).unwrappedValue.children }),
196+
}
197+
182198
/**
183199
* Loads a CSV file and returns its content as a display-ready table.
184200
* @param path path of the CSV file (with extension) to show
201+
* @param mode mode to handle the content of each cell and header (plain or Markdown)
185202
* @param caption optional caption of the table. If set, the table will be numbered according to the current [numbering] format
186203
* @return a table whose content is loaded from the file located in [path]
187204
* @wiki File data
188205
*/
189206
fun csv(
190207
@Injected context: Context,
191208
path: String,
209+
mode: CsvParsingMode = CsvParsingMode.PLAIN,
192210
@LikelyNamed caption: String? = null,
193211
): NodeValue {
194212
val file = file(context, path)
195-
val columns = mutableMapOf<String, MutableList<String>>()
213+
val columns = mutableListOf<Table.MutableColumn>()
196214

197215
// CSV is read row-by-row, while the Table is built by columns.
198216
csvReader().open(file) {
199217
readAllWithHeaderAsSequence()
200-
.flatMap { it.entries }
201-
.forEach { (header, content) ->
202-
val cells = columns.computeIfAbsent(header) { mutableListOf() }
203-
cells += content
218+
.forEach { row ->
219+
row.entries.forEachIndexed { index, (header, content) ->
220+
val cell = mode.transform(content.trim(), context).let(Table::Cell)
221+
222+
if (index < columns.size) {
223+
// Adding cell to existing column.
224+
columns[index].cells += cell
225+
} else {
226+
// Pushing new column.
227+
val headerCell = mode.transform(header.trim(), context).let(Table::Cell)
228+
columns +=
229+
Table.MutableColumn(
230+
alignment = Table.Alignment.NONE,
231+
header = headerCell,
232+
cells = mutableListOf(cell),
233+
)
234+
}
235+
}
204236
}
205237
}
206238

207-
val table =
208-
Table(
209-
columns.map { (header, cells) ->
210-
Table.Column(
211-
Table.Alignment.NONE,
212-
Table.Cell(listOf(Text(header.trim()))),
213-
cells.map { cell -> Table.Cell(listOf(Text(cell.trim()))) },
214-
)
215-
},
216-
caption,
217-
)
218-
219-
return table.wrappedAsValue()
239+
return Table(
240+
columns.map { it.toColumn() },
241+
caption,
242+
).wrappedAsValue()
220243
}

quarkdown-stdlib/src/test/kotlin/com/quarkdown/stdlib/DataTest.kt

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package com.quarkdown.stdlib
22

3+
import com.quarkdown.core.assertNodeEquals
34
import com.quarkdown.core.ast.base.block.Table
45
import com.quarkdown.core.ast.base.inline.Text
6+
import com.quarkdown.core.ast.dsl.buildInline
57
import com.quarkdown.core.attachMockPipeline
68
import com.quarkdown.core.context.MutableContext
79
import com.quarkdown.core.flavor.quarkdown.QuarkdownFlavor
@@ -150,6 +152,58 @@ class DataTest {
150152
}
151153
}
152154

155+
@Test
156+
fun `csv table, as plain text cells`() {
157+
val path = "drinks.csv"
158+
val table = csv(context, path, mode = CsvParsingMode.PLAIN).unwrappedValue
159+
assertIs<Table>(table)
160+
161+
val columns = table.columns.iterator()
162+
with(columns.next()) {
163+
assertEquals("Name", (header.text.first() as Text).text)
164+
with(cells.iterator()) {
165+
assertEquals("Alice", (next().text.first() as Text).text)
166+
assertEquals("Bob", (next().text.first() as Text).text)
167+
}
168+
}
169+
with(columns.next()) {
170+
assertEquals("*Favorite* drink", (header.text.first() as Text).text)
171+
with(cells.iterator()) {
172+
assertEquals("**Coffee**", (next().text.first() as Text).text)
173+
assertEquals("***Pepsi***", (next().text.first() as Text).text)
174+
}
175+
}
176+
}
177+
178+
@Test
179+
fun `csv table, as markdown cells`() {
180+
val path = "drinks.csv"
181+
val table = csv(context, path, mode = CsvParsingMode.MARKDOWN).unwrappedValue
182+
assertIs<Table>(table)
183+
184+
val columns = table.columns.iterator()
185+
with(columns.next()) {
186+
assertEquals("Name", (header.text.first() as Text).text)
187+
with(cells.iterator()) {
188+
assertEquals("Alice", (next().text.first() as Text).text)
189+
assertEquals("Bob", (next().text.first() as Text).text)
190+
}
191+
}
192+
with(columns.next()) {
193+
assertNodeEquals(
194+
buildInline {
195+
emphasis { text("Favorite") }
196+
text(" drink")
197+
},
198+
header.text,
199+
)
200+
with(cells.iterator()) {
201+
assertNodeEquals(buildInline { strong { text("Coffee") } }, next().text)
202+
assertNodeEquals(buildInline { strongEmphasis { text("Pepsi") } }, next().text)
203+
}
204+
}
205+
}
206+
153207
@Test
154208
fun `list files unsorted`() {
155209
val files = listFiles(context, LIST_FILES_FOLDER, fullPath = false)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Name, *Favorite* drink
2+
Alice, **Coffee**
3+
Bob, ***Pepsi***

quarkdown-test/src/test/kotlin/com/quarkdown/test/TableComputationTest.kt

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -107,11 +107,17 @@ class TableComputationTest {
107107

108108
@Test
109109
fun csv() {
110-
execute(".csv {csv/people.csv}") {
111-
assertEquals(
112-
htmlTable(john + lisa + mike),
113-
it,
114-
)
110+
arrayOf(
111+
".csv {csv/people.csv}",
112+
".csv {csv/people.csv} mode:{plain}",
113+
".csv {csv/people.csv} mode:{markdown}",
114+
).forEach { source ->
115+
execute(source) {
116+
assertEquals(
117+
htmlTable(john + lisa + mike),
118+
it,
119+
)
120+
}
115121
}
116122
}
117123

@@ -128,6 +134,21 @@ class TableComputationTest {
128134
}
129135
}
130136

137+
@Test
138+
fun `csv with markup and function calls`() {
139+
execute(".csv {csv/sums.csv} mode:{markdown}") {
140+
assertEquals(
141+
"<table><thead>" +
142+
"<tr><th></th><th>1</th><th>2</th><th>3</th></tr></thead><tbody>" +
143+
"<tr><td><strong>1</strong></td><td>2</td><td>3</td><td>4</td></tr>" +
144+
"<tr><td><strong>2</strong></td><td>3</td><td>4</td><td>5</td></tr>" +
145+
"<tr><td><strong>3</strong></td><td>4</td><td>5</td><td>6</td></tr>" +
146+
"</tbody></table>",
147+
it,
148+
)
149+
}
150+
}
151+
131152
@Test
132153
fun `compute on csv`() {
133154
execute(".tablecompute {2} {@lambda x: .x::average::round}\n\t.csv {csv/people.csv}") {
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
, 1, 2, 3
2+
**1** , .sum {1} {1}, .sum {1} {2}, .sum {1} {3}
3+
**2** , .sum {2} {1}, .sum {2} {2}, .sum {2} {3}
4+
**3** , .sum {3} {1}, .sum {3} {2}, .sum {3} {3}

0 commit comments

Comments
 (0)