Skip to content

Commit 4785dec

Browse files
committed
YAML front matter: Limited support for single/double quoted strings
Extend our manual parser to handle string values that use single or double quotes. The support is limited and doesn't implement the full YAML spec (e.g. no support for escapes like `\n`). At some point we should either depend on a real YAML parser or expose the raw YAML source so that users can parse it themselves. Fixes #260.
1 parent 7778446 commit 4785dec

File tree

3 files changed

+83
-65
lines changed

3 files changed

+83
-65
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
66
This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html),
77
with the exception that 0.x versions can break between minor versions.
88

9+
## Unreleased
10+
### Added
11+
- YAML front matter extension: Limited support for single and double
12+
quoted string values (#260)
13+
914
## [0.18.2] - 2022-02-24
1015
### Changed
1116
- Test against Java 17

commonmark-ext-yaml-front-matter/src/main/java/org/commonmark/ext/front/matter/internal/YamlFrontMatterBlockParser.java

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,11 @@ public BlockContinue tryContinue(ParserState parserState) {
6161
inLiteral = false;
6262
currentKey = matcher.group(1);
6363
currentValues = new ArrayList<>();
64-
if ("|".equals(matcher.group(2))) {
64+
String value = matcher.group(2);
65+
if ("|".equals(value)) {
6566
inLiteral = true;
66-
} else if (!"".equals(matcher.group(2))) {
67-
currentValues.add(matcher.group(2));
67+
} else if (!"".equals(value)) {
68+
currentValues.add(parseString(value));
6869
}
6970

7071
return BlockContinue.atIndex(parserState.getIndex());
@@ -81,7 +82,8 @@ public BlockContinue tryContinue(ParserState parserState) {
8182
} else {
8283
matcher = REGEX_METADATA_LIST.matcher(line);
8384
if (matcher.matches()) {
84-
currentValues.add(matcher.group(1));
85+
String value = matcher.group(1);
86+
currentValues.add(parseString(value));
8587
}
8688
}
8789

@@ -93,6 +95,24 @@ public BlockContinue tryContinue(ParserState parserState) {
9395
public void parseInlines(InlineParser inlineParser) {
9496
}
9597

98+
private static String parseString(String s) {
99+
// Limited parsing of https://yaml.org/spec/1.2.2/#73-flow-scalar-styles
100+
// We assume input is well-formed and otherwise treat it as a plain string. In a real
101+
// parser, e.g. `'foo` would be invalid because it's missing a trailing `'`.
102+
if (s.startsWith("'") && s.endsWith("'")) {
103+
String inner = s.substring(1, s.length() - 1);
104+
return inner.replace("''", "'");
105+
} else if (s.startsWith("\"") && s.endsWith("\"")) {
106+
String inner = s.substring(1, s.length() - 1);
107+
// Only support escaped `\` and `"`, nothing else.
108+
return inner
109+
.replace("\\\"", "\"")
110+
.replace("\\\\", "\\");
111+
} else {
112+
return s;
113+
}
114+
}
115+
96116
public static class Factory extends AbstractBlockParserFactory {
97117
@Override
98118
public BlockStart tryStart(ParserState state, MatchedBlockParser matchedBlockParser) {

commonmark-ext-yaml-front-matter/src/test/java/org/commonmark/ext/front/matter/YamlFrontMatterTest.java

Lines changed: 54 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,7 @@ public void simpleValue() {
3030
"\ngreat";
3131
final String rendered = "<p>great</p>\n";
3232

33-
YamlFrontMatterVisitor visitor = new YamlFrontMatterVisitor();
34-
Node document = PARSER.parse(input);
35-
document.accept(visitor);
36-
37-
Map<String, List<String>> data = visitor.getData();
33+
Map<String, List<String>> data = getFrontMatter(input);
3834

3935
assertEquals(1, data.size());
4036
assertEquals("hello", data.keySet().iterator().next());
@@ -53,11 +49,7 @@ public void emptyValue() {
5349
"\ngreat";
5450
final String rendered = "<p>great</p>\n";
5551

56-
YamlFrontMatterVisitor visitor = new YamlFrontMatterVisitor();
57-
Node document = PARSER.parse(input);
58-
document.accept(visitor);
59-
60-
Map<String, List<String>> data = visitor.getData();
52+
Map<String, List<String>> data = getFrontMatter(input);
6153

6254
assertEquals(1, data.size());
6355
assertEquals("key", data.keySet().iterator().next());
@@ -77,11 +69,7 @@ public void listValues() {
7769
"\ngreat";
7870
final String rendered = "<p>great</p>\n";
7971

80-
YamlFrontMatterVisitor visitor = new YamlFrontMatterVisitor();
81-
Node document = PARSER.parse(input);
82-
document.accept(visitor);
83-
84-
Map<String, List<String>> data = visitor.getData();
72+
Map<String, List<String>> data = getFrontMatter(input);
8573

8674
assertEquals(1, data.size());
8775
assertTrue(data.containsKey("list"));
@@ -103,11 +91,7 @@ public void literalValue1() {
10391
"\ngreat";
10492
final String rendered = "<p>great</p>\n";
10593

106-
YamlFrontMatterVisitor visitor = new YamlFrontMatterVisitor();
107-
Node document = PARSER.parse(input);
108-
document.accept(visitor);
109-
110-
Map<String, List<String>> data = visitor.getData();
94+
Map<String, List<String>> data = getFrontMatter(input);
11195

11296
assertEquals(1, data.size());
11397
assertTrue(data.containsKey("literal"));
@@ -127,11 +111,7 @@ public void literalValue2() {
127111
"\ngreat";
128112
final String rendered = "<p>great</p>\n";
129113

130-
YamlFrontMatterVisitor visitor = new YamlFrontMatterVisitor();
131-
Node document = PARSER.parse(input);
132-
document.accept(visitor);
133-
134-
Map<String, List<String>> data = visitor.getData();
114+
Map<String, List<String>> data = getFrontMatter(input);
135115

136116
assertEquals(1, data.size());
137117
assertTrue(data.containsKey("literal"));
@@ -156,11 +136,7 @@ public void complexValues() {
156136
"\ngreat";
157137
final String rendered = "<p>great</p>\n";
158138

159-
YamlFrontMatterVisitor visitor = new YamlFrontMatterVisitor();
160-
Node document = PARSER.parse(input);
161-
document.accept(visitor);
162-
163-
Map<String, List<String>> data = visitor.getData();
139+
Map<String, List<String>> data = getFrontMatter(input);
164140

165141
assertEquals(3, data.size());
166142

@@ -187,11 +163,7 @@ public void empty() {
187163
"test";
188164
final String rendered = "<p>test</p>\n";
189165

190-
YamlFrontMatterVisitor visitor = new YamlFrontMatterVisitor();
191-
Node document = PARSER.parse(input);
192-
document.accept(visitor);
193-
194-
Map<String, List<String>> data = visitor.getData();
166+
Map<String, List<String>> data = getFrontMatter(input);
195167

196168
assertTrue(data.isEmpty());
197169

@@ -207,11 +179,7 @@ public void yamlInParagraph() {
207179
"\n---";
208180
final String rendered = "<h1>hello</h1>\n<h2>hello markdown world!</h2>\n<h2>hello: world</h2>\n";
209181

210-
YamlFrontMatterVisitor visitor = new YamlFrontMatterVisitor();
211-
Node document = PARSER.parse(input);
212-
document.accept(visitor);
213-
214-
Map<String, List<String>> data = visitor.getData();
182+
Map<String, List<String>> data = getFrontMatter(input);
215183

216184
assertTrue(data.isEmpty());
217185

@@ -226,11 +194,7 @@ public void yamlOnSecondLine() {
226194
"\n---";
227195
final String rendered = "<p>hello</p>\n<hr />\n<h2>hello: world</h2>\n";
228196

229-
YamlFrontMatterVisitor visitor = new YamlFrontMatterVisitor();
230-
Node document = PARSER.parse(input);
231-
document.accept(visitor);
232-
233-
Map<String, List<String>> data = visitor.getData();
197+
Map<String, List<String>> data = getFrontMatter(input);
234198

235199
assertTrue(data.isEmpty());
236200

@@ -243,11 +207,7 @@ public void nonMatchedStartTag() {
243207
"test";
244208
final String rendered = "<hr />\n<p>test</p>\n";
245209

246-
YamlFrontMatterVisitor visitor = new YamlFrontMatterVisitor();
247-
Node document = PARSER.parse(input);
248-
document.accept(visitor);
249-
250-
Map<String, List<String>> data = visitor.getData();
210+
Map<String, List<String>> data = getFrontMatter(input);
251211

252212
assertTrue(data.isEmpty());
253213

@@ -261,11 +221,7 @@ public void inList() {
261221
"test";
262222
final String rendered = "<ul>\n<li>\n<hr />\n<hr />\n</li>\n</ul>\n<p>test</p>\n";
263223

264-
YamlFrontMatterVisitor visitor = new YamlFrontMatterVisitor();
265-
Node document = PARSER.parse(input);
266-
document.accept(visitor);
267-
268-
Map<String, List<String>> data = visitor.getData();
224+
Map<String, List<String>> data = getFrontMatter(input);
269225

270226
assertTrue(data.isEmpty());
271227

@@ -310,31 +266,68 @@ public void nodesCanBeModified() {
310266
assertTrue(data.containsKey("see"));
311267
assertEquals(Collections.singletonList("you"), data.get("see"));
312268
}
313-
269+
314270
@Test
315271
public void dotInKeys() {
316272
final String input = "---" +
317273
"\nms.author: author" +
318274
"\n---" +
319275
"\n";
320276

321-
YamlFrontMatterVisitor visitor = new YamlFrontMatterVisitor();
322-
Node document = PARSER.parse(input);
323-
document.accept(visitor);
324-
325-
Map<String, List<String>> data = visitor.getData();
277+
Map<String, List<String>> data = getFrontMatter(input);
326278

327279
assertEquals(1, data.size());
328280
assertEquals("ms.author", data.keySet().iterator().next());
329281
assertEquals(1, data.get("ms.author").size());
330282
assertEquals("author", data.get("ms.author").get(0));
331283
}
332284

285+
@Test
286+
public void singleQuotedLiterals() {
287+
final String input = "---" +
288+
"\nstring: 'It''s me'" +
289+
"\nlist:" +
290+
"\n - 'I''m here'" +
291+
"\n---" +
292+
"\n";
293+
294+
Map<String, List<String>> data = getFrontMatter(input);
295+
296+
assertEquals(2, data.size());
297+
assertEquals("It's me", data.get("string").get(0));
298+
assertEquals("I'm here", data.get("list").get(0));
299+
}
300+
301+
@Test
302+
public void doubleQuotedLiteral() {
303+
final String input = "---" +
304+
"\nstring: \"backslash: \\\\ quote: \\\"\"" +
305+
"\nlist:" +
306+
"\n - \"hey\"" +
307+
"\n---" +
308+
"\n";
309+
310+
Map<String, List<String>> data = getFrontMatter(input);
311+
312+
assertEquals(2, data.size());
313+
assertEquals("backslash: \\ quote: \"", data.get("string").get(0));
314+
assertEquals("hey", data.get("list").get(0));
315+
}
316+
333317
@Override
334318
protected String render(String source) {
335319
return RENDERER.render(PARSER.parse(source));
336320
}
337321

322+
private Map<String, List<String>> getFrontMatter(String input) {
323+
YamlFrontMatterVisitor visitor = new YamlFrontMatterVisitor();
324+
Node document = PARSER.parse(input);
325+
document.accept(visitor);
326+
327+
Map<String, List<String>> data = visitor.getData();
328+
return data;
329+
}
330+
338331
// Custom node for tests
339332
private static class TestNode extends CustomNode {
340333
}

0 commit comments

Comments
 (0)