-
-
Notifications
You must be signed in to change notification settings - Fork 333
Better YAML front matter parsing in commonmark-ext-yaml-front-matter #445
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| package org.commonmark.ext.front.matter; | ||
|
|
||
| import org.commonmark.node.CustomNode; | ||
|
|
||
| public class YamlFrontMatterContent extends CustomNode { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So the existing one is called |
||
| private String content; | ||
|
|
||
| public YamlFrontMatterContent(String content) { | ||
| this.content = content; | ||
| } | ||
|
|
||
| public String getContent() { | ||
| return content; | ||
| } | ||
|
|
||
| public void setContent(String content) { | ||
| this.content = content; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| package org.commonmark.ext.front.matter; | ||
|
|
||
| import org.commonmark.parser.block.BlockContinue; | ||
|
|
||
| public interface YamlFrontMatterExtractor { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm not sure about calling this an "extractor", it feels like what the visitor does sounds more like extracting. We already have a good word for turning some input into nodes, which is a parser. Should we just call this For the name of the implementations I have similar concerns (them not being specific enough). With the above change, they would be:
(Note that the raw content parser could also be used for e.g. TOML or other syntax of front matter.)
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You're right, I struggled to find the right naming here. I initially rejected "parser", because I had a very similar name |
||
| void onNextLine(YamlFrontMatterBlock block, CharSequence line); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you change the line to |
||
|
|
||
| BlockContinue onBlockEnd(YamlFrontMatterBlock block); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would remove the return type here, as anything else than
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually, it allows the parser to handle the following use case described here, if someone needs it: But this is a minor issue with easy workaround, so I can either simplify the interface or document the purpose in the javadoc. |
||
|
|
||
| interface Factory { | ||
| YamlFrontMatterExtractor create(); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -3,28 +3,89 @@ | |||||
| import java.util.LinkedHashMap; | ||||||
| import java.util.List; | ||||||
| import java.util.Map; | ||||||
|
|
||||||
| import org.commonmark.ext.front.matter.extractor.YamlDataExtractor; | ||||||
| import org.commonmark.ext.front.matter.extractor.YamlContentExtractor; | ||||||
| import org.commonmark.node.AbstractVisitor; | ||||||
| import org.commonmark.node.CustomNode; | ||||||
| import org.commonmark.node.Node; | ||||||
|
|
||||||
| public class YamlFrontMatterVisitor extends AbstractVisitor { | ||||||
| private boolean present; | ||||||
| private Map<String, List<String>> data; | ||||||
| private String content; | ||||||
|
|
||||||
| public YamlFrontMatterVisitor() { | ||||||
| data = new LinkedHashMap<>(); | ||||||
| content = ""; | ||||||
| present = false; | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * Reads the YAML front matter metadata, if the Markdown | ||||||
| * document has the YAML front matter and the extension | ||||||
| * uses {@link YamlDataExtractor} (default). | ||||||
| * | ||||||
| * @return The data stored in YAML front matter or empty map | ||||||
| */ | ||||||
| public static Map<String, List<String>> readData(Node document) { | ||||||
| YamlFrontMatterVisitor visitor = new YamlFrontMatterVisitor(); | ||||||
| document.accept(visitor); | ||||||
| return visitor.getData(); | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * Reads the YAML Front Matter metadata as a string, if the Markdown | ||||||
| * document has the YAML Front Matter and the extension uses | ||||||
| * {@link YamlContentExtractor} (default). | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| * | ||||||
| * @return The content of YAML front matter as string or empty string. | ||||||
| */ | ||||||
| public static String readContent(Node document) { | ||||||
| YamlFrontMatterVisitor visitor = new YamlFrontMatterVisitor(); | ||||||
| document.accept(visitor); | ||||||
| return visitor.getContent(); | ||||||
| } | ||||||
|
|
||||||
| @Override | ||||||
| public void visit(CustomNode customNode) { | ||||||
| if (customNode instanceof YamlFrontMatterNode) { | ||||||
| data.put( | ||||||
| ((YamlFrontMatterNode) customNode).getKey(), | ||||||
| ((YamlFrontMatterNode) customNode).getValues()); | ||||||
| ((YamlFrontMatterNode) customNode).getValues() | ||||||
| ); | ||||||
| present = true; | ||||||
| } else if (customNode instanceof YamlFrontMatterContent) { | ||||||
| content = ((YamlFrontMatterContent) customNode).getContent(); | ||||||
| present = true; | ||||||
| } else { | ||||||
| super.visit(customNode); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * Returns the YAML front matter metadata, if the Markdown document has | ||||||
| * the YAML front matter and the extension uses {@link YamlDataExtractor} | ||||||
| * (default). | ||||||
| * | ||||||
| * @return The data stored in YAML front matter or empty map | ||||||
| */ | ||||||
| public Map<String, List<String>> getData() { | ||||||
| return data; | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * Returns the YAML Front Matter metadata as a string, if the Markdown | ||||||
| * document has the YAML Front Matter and the extension uses | ||||||
| * {@link YamlContentExtractor} (default). | ||||||
| * | ||||||
| * @return The content of YAML front matter as string or empty string. | ||||||
| */ | ||||||
| public String getContent() { | ||||||
| return content; | ||||||
| } | ||||||
|
|
||||||
| public boolean isFrontMatterPresent() { | ||||||
| return present; | ||||||
| } | ||||||
| } | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| package org.commonmark.ext.front.matter.extractor; | ||
|
|
||
| import org.commonmark.ext.front.matter.YamlFrontMatterExtractor; | ||
| import org.commonmark.ext.front.matter.YamlFrontMatterBlock; | ||
| import org.commonmark.ext.front.matter.YamlFrontMatterContent; | ||
| import org.commonmark.parser.block.BlockContinue; | ||
|
|
||
| public class YamlContentExtractor implements YamlFrontMatterExtractor { | ||
| private StringBuilder content; | ||
|
|
||
| public YamlContentExtractor() { | ||
| content = new StringBuilder(); | ||
| } | ||
|
|
||
| @Override | ||
| public void onNextLine(YamlFrontMatterBlock block, CharSequence line) { | ||
| content.append(line).append('\n'); | ||
| } | ||
|
|
||
| @Override | ||
| public BlockContinue onBlockEnd(YamlFrontMatterBlock block) { | ||
| block.appendChild(new YamlFrontMatterContent(content.toString())); | ||
| return BlockContinue.finished(); | ||
| } | ||
|
|
||
| public static class Factory implements YamlFrontMatterExtractor.Factory { | ||
| @Override | ||
| public YamlFrontMatterExtractor create() { | ||
| return new YamlContentExtractor(); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| package org.commonmark.ext.front.matter.extractor; | ||
|
|
||
| import org.commonmark.ext.front.matter.YamlFrontMatterExtractor; | ||
| import org.commonmark.ext.front.matter.YamlFrontMatterBlock; | ||
| import org.commonmark.ext.front.matter.YamlFrontMatterNode; | ||
| import org.commonmark.parser.block.BlockContinue; | ||
|
|
||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
| import java.util.regex.Matcher; | ||
| import java.util.regex.Pattern; | ||
|
|
||
| public class YamlDataExtractor implements YamlFrontMatterExtractor { | ||
| private static final Pattern REGEX_METADATA = | ||
| Pattern.compile("^[ ]{0,3}([A-Za-z0-9._-]+):\\s*(.*)"); | ||
| private static final Pattern REGEX_METADATA_LIST = Pattern.compile("^[ ]+-\\s*(.*)"); | ||
| private static final Pattern REGEX_METADATA_LITERAL = Pattern.compile("^\\s*(.*)"); | ||
|
|
||
| private boolean inLiteral; | ||
| private String currentKey; | ||
| private List<String> currentValues; | ||
|
|
||
| public YamlDataExtractor() { | ||
| inLiteral = false; | ||
| currentKey = null; | ||
| currentValues = new ArrayList<>(); | ||
| } | ||
|
|
||
| @Override | ||
| public void onNextLine(YamlFrontMatterBlock block, CharSequence line) { | ||
| Matcher matcher = REGEX_METADATA.matcher(line); | ||
| if (matcher.matches()) { | ||
| if (currentKey != null) { | ||
| block.appendChild(new YamlFrontMatterNode(currentKey, currentValues)); | ||
| } | ||
|
|
||
| inLiteral = false; | ||
| currentKey = matcher.group(1); | ||
| currentValues = new ArrayList<>(); | ||
| String value = matcher.group(2); | ||
| if ("|".equals(value)) { | ||
| inLiteral = true; | ||
| } else if (!"".equals(value)) { | ||
| currentValues.add(parseString(value)); | ||
| } | ||
| } else { | ||
| if (inLiteral) { | ||
| matcher = REGEX_METADATA_LITERAL.matcher(line); | ||
| if (matcher.matches()) { | ||
| if (currentValues.size() == 1) { | ||
| currentValues.set(0, currentValues.get(0) + "\n" + matcher.group(1).trim()); | ||
| } else { | ||
| currentValues.add(matcher.group(1).trim()); | ||
| } | ||
| } | ||
| } else { | ||
| matcher = REGEX_METADATA_LIST.matcher(line); | ||
| if (matcher.matches()) { | ||
| String value = matcher.group(1); | ||
| currentValues.add(parseString(value)); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| @Override | ||
| public BlockContinue onBlockEnd(YamlFrontMatterBlock block) { | ||
| if (currentKey != null) { | ||
| block.appendChild(new YamlFrontMatterNode(currentKey, currentValues)); | ||
| } | ||
| return BlockContinue.finished(); | ||
| } | ||
|
|
||
| private static String parseString(String s) { | ||
| // Limited parsing of https://yaml.org/spec/1.2.2/#73-flow-scalar-styles | ||
| // We assume input is well-formed and otherwise treat it as a plain string. In a real | ||
| // parser, e.g. `'foo` would be invalid because it's missing a trailing `'`. | ||
| if (s.startsWith("'") && s.endsWith("'")) { | ||
| String inner = s.substring(1, s.length() - 1); | ||
| return inner.replace("''", "'"); | ||
| } else if (s.startsWith("\"") && s.endsWith("\"")) { | ||
| String inner = s.substring(1, s.length() - 1); | ||
| // Only support escaped `\` and `"`, nothing else. | ||
| return inner.replace("\\\"", "\"").replace("\\\\", "\\"); | ||
| } else { | ||
| return s; | ||
| } | ||
| } | ||
|
|
||
| public static class Factory implements YamlFrontMatterExtractor.Factory { | ||
| @Override | ||
| public YamlFrontMatterExtractor create() { | ||
| return new YamlDataExtractor(); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo "use initialize"