| /******************************************************************************* |
| * Copyright (c) 2014, 2015 Tasktop Technologies. |
| * All rights reserved. This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License v1.0 |
| * which accompanies this distribution, and is available at |
| * http://www.eclipse.org/legal/epl-v10.html |
| * |
| * Contributors: |
| * Leo Dos Santos - initial API and implementation |
| *******************************************************************************/ |
| |
| package org.eclipse.mylyn.wikitext.markdown.internal; |
| |
| import static com.google.common.base.Preconditions.checkNotNull; |
| |
| import java.io.IOException; |
| import java.io.StringWriter; |
| import java.io.Writer; |
| import java.util.HashMap; |
| import java.util.Map; |
| import java.util.logging.Logger; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| import org.eclipse.mylyn.wikitext.markdown.MarkdownLanguage; |
| import org.eclipse.mylyn.wikitext.parser.Attributes; |
| import org.eclipse.mylyn.wikitext.parser.ImageAttributes; |
| import org.eclipse.mylyn.wikitext.parser.LinkAttributes; |
| import org.eclipse.mylyn.wikitext.parser.builder.AbstractMarkupDocumentBuilder; |
| |
| import com.google.common.base.CharMatcher; |
| import com.google.common.base.Strings; |
| |
| /** |
| * a document builder that emits Markdown markup |
| * |
| * @author Leo Dos Santos |
| * @see MarkdownLanguage |
| * @see MarkdownLanguage#createDocumentBuilder(Writer) |
| */ |
| public class MarkdownDocumentBuilder extends AbstractMarkupDocumentBuilder { |
| |
| private static final Pattern PATTERN_LINE_BREAK = Pattern.compile("(.*(\r\n|\r|\n)?)?"); //$NON-NLS-1$ |
| |
| private final Map<String, String> entityToLiteral = new HashMap<String, String>(); |
| { |
| entityToLiteral.put("amp", "&"); //$NON-NLS-1$ //$NON-NLS-2$ |
| entityToLiteral.put("lt", "<"); //$NON-NLS-1$ //$NON-NLS-2$ |
| entityToLiteral.put("gt", ">"); //$NON-NLS-1$ //$NON-NLS-2$ |
| } |
| |
| private interface MarkdownBlock { |
| |
| void lineBreak() throws IOException; |
| |
| } |
| |
| private class ContentBlock extends NewlineDelimitedBlock implements MarkdownBlock { |
| |
| protected String prefix; |
| |
| protected String suffix; |
| |
| ContentBlock(BlockType blockType, String prefix, String suffix, int leadingNewlines, int trailingNewlines) { |
| super(blockType, leadingNewlines, trailingNewlines); |
| this.prefix = prefix; |
| this.suffix = suffix; |
| } |
| |
| ContentBlock(String prefix, String suffix, int leadingNewlines, int trailingNewlines) { |
| this(null, prefix, suffix, leadingNewlines, trailingNewlines); |
| } |
| |
| @Override |
| public void write(int c) throws IOException { |
| MarkdownDocumentBuilder.this.emitContent(c); |
| } |
| |
| @Override |
| public void write(String s) throws IOException { |
| MarkdownDocumentBuilder.this.emitContent(s); |
| } |
| |
| @Override |
| public void lineBreak() throws IOException { |
| write(" \n"); //$NON-NLS-1$ |
| } |
| |
| @Override |
| public void open() throws IOException { |
| super.open(); |
| pushWriter(new StringWriter()); |
| } |
| |
| @Override |
| public void close() throws IOException { |
| Writer thisContent = popWriter(); |
| String content = thisContent.toString(); |
| if (content.length() > 0) { |
| emitContent(content); |
| } |
| super.close(); |
| } |
| |
| protected void emitContent(final String content) throws IOException { |
| MarkdownDocumentBuilder.this.emitContent(prefix); |
| MarkdownDocumentBuilder.this.emitContent(content); |
| MarkdownDocumentBuilder.this.emitContent(suffix); |
| } |
| |
| } |
| |
| private class ImplicitParagraphBlock extends ContentBlock { |
| |
| ImplicitParagraphBlock() { |
| super(BlockType.PARAGRAPH, "", "", 2, 2); //$NON-NLS-1$ //$NON-NLS-2$ |
| } |
| |
| @Override |
| protected boolean isImplicitBlock() { |
| return true; |
| } |
| |
| } |
| |
| private class PrefixedLineContentBlock extends ContentBlock { |
| |
| PrefixedLineContentBlock(BlockType blockType, String prefix, String suffix, int leadingNewlines, |
| int trailingNewlines) { |
| super(blockType, prefix, suffix, leadingNewlines, trailingNewlines); |
| } |
| |
| @Override |
| protected void emitContent(String content) throws IOException { |
| // break out the block onto its own line if the last character |
| // was not a line break or null character literal |
| char lastChar = getLastChar(); |
| if (lastChar != '\n' && lastChar != '\r' && lastChar != '\u0000') { |
| MarkdownDocumentBuilder.this.emitContent('\n'); |
| } |
| |
| // split out content by line break |
| Matcher matcher = PATTERN_LINE_BREAK.matcher(content); |
| while (matcher.find()) { |
| // if the line is empty, emit no prefix |
| String line = matcher.group(0); |
| if (!line.trim().isEmpty()) { |
| MarkdownDocumentBuilder.this.emitContent(prefix); |
| } |
| MarkdownDocumentBuilder.this.emitContent(line); |
| } |
| // collapse suffix for nested blocks |
| if (!content.endsWith(suffix)) { |
| MarkdownDocumentBuilder.this.emitContent(suffix); |
| } |
| } |
| |
| } |
| |
| private class ListBlock extends ContentBlock { |
| |
| private int count = 0; |
| |
| ListBlock(BlockType blockType, int leadingNewlines) { |
| super(blockType, "", "", leadingNewlines, 1); //$NON-NLS-1$//$NON-NLS-2$ |
| } |
| |
| @Override |
| protected void emitContent(String content) throws IOException { |
| MarkdownDocumentBuilder.this.emitContent(prefix); |
| MarkdownDocumentBuilder.this.emitContent(content); |
| if (!content.endsWith("\n\n")) { //$NON-NLS-1$ |
| MarkdownDocumentBuilder.this.emitContent(suffix); |
| } |
| } |
| |
| protected void addListItem(ListItemBlock item) { |
| checkNotNull(item); |
| count++; |
| } |
| |
| protected int getCount() { |
| return count; |
| } |
| |
| } |
| |
| private class ListItemBlock extends ContentBlock { |
| |
| private int count; |
| |
| private ListItemBlock(String prefix) { |
| super(BlockType.LIST_ITEM, prefix, "", 1, 1); //$NON-NLS-1$ |
| } |
| |
| @Override |
| public void open() throws IOException { |
| super.open(); |
| if (getPreviousBlock() instanceof ListBlock) { |
| ListBlock list = (ListBlock) getPreviousBlock(); |
| list.addListItem(this); |
| count = list.getCount(); |
| } |
| } |
| |
| @Override |
| protected void emitContent(String content) throws IOException { |
| if (getPreviousBlock().getBlockType() == BlockType.NUMERIC_LIST) { |
| prefix = count + ". "; //$NON-NLS-1$ |
| } |
| String indent = Strings.repeat(" ", prefix.length()); //$NON-NLS-1$ |
| |
| MarkdownDocumentBuilder.this.emitContent(prefix); |
| // split out content by line |
| Matcher matcher = PATTERN_LINE_BREAK.matcher(content); |
| int lines = 0; |
| while (matcher.find()) { |
| // indent each line hanging past the initial line item |
| String line = matcher.group(0); |
| if (lines > 0 && !line.trim().isEmpty()) { |
| int indexOfFirstNonSpace = CharMatcher.isNot(' ').indexIn(line); |
| if (indexOfFirstNonSpace >= 4) { |
| line = Strings.repeat(" ", 4) + line; //$NON-NLS-1$ |
| } else { |
| line = indent + line; |
| } |
| } |
| MarkdownDocumentBuilder.this.emitContent(line); |
| lines++; |
| } |
| // collapse suffix for nested blocks |
| if (!content.endsWith(suffix)) { |
| MarkdownDocumentBuilder.this.emitContent(suffix); |
| } |
| } |
| |
| } |
| |
| private class LinkBlock extends ContentBlock { |
| |
| private final LinkAttributes attributes; |
| |
| LinkBlock(LinkAttributes attributes) { |
| super("", "", 0, 0); //$NON-NLS-1$ //$NON-NLS-2$ |
| this.attributes = attributes; |
| } |
| |
| @Override |
| protected void emitContent(String content) throws IOException { |
| // [label](http://url.com) or |
| // [label](http://url.com "title") |
| MarkdownDocumentBuilder.this.emitContent('['); |
| MarkdownDocumentBuilder.this.emitContent(content); |
| MarkdownDocumentBuilder.this.emitContent(']'); |
| |
| MarkdownDocumentBuilder.this.emitContent('('); |
| MarkdownDocumentBuilder.this.emitContent(attributes.getHref()); |
| if (!Strings.isNullOrEmpty(attributes.getTitle())) { |
| MarkdownDocumentBuilder.this.emitContent(" \""); //$NON-NLS-1$ |
| MarkdownDocumentBuilder.this.emitContent(attributes.getTitle()); |
| MarkdownDocumentBuilder.this.emitContent('"'); |
| } |
| MarkdownDocumentBuilder.this.emitContent(')'); |
| } |
| |
| } |
| |
| private class CodeSpan extends ContentBlock { |
| |
| private CodeSpan() { |
| super("`", "`", 0, 0); //$NON-NLS-1$ //$NON-NLS-2$ |
| } |
| |
| @Override |
| protected void emitContent(String content) throws IOException { |
| if (content.contains("`")) { //$NON-NLS-1$ |
| prefix = "`` "; //$NON-NLS-1$ |
| suffix = " ``"; //$NON-NLS-1$ |
| } |
| super.emitContent(content); |
| } |
| |
| } |
| |
| public MarkdownDocumentBuilder(Writer out) { |
| super(out); |
| currentBlock = null; |
| } |
| |
| @Override |
| protected Block computeBlock(BlockType type, Attributes attributes) { |
| switch (type) { |
| case PARAGRAPH: |
| return new ContentBlock(type, "", "", 2, 2); //$NON-NLS-1$ //$NON-NLS-2$ |
| case QUOTE: |
| return new PrefixedLineContentBlock(type, "> ", "", 1, 1); //$NON-NLS-1$ //$NON-NLS-2$ |
| case BULLETED_LIST: |
| case NUMERIC_LIST: |
| if (currentBlock != null) { |
| BlockType currentBlockType = currentBlock.getBlockType(); |
| if (currentBlockType == BlockType.LIST_ITEM || currentBlockType == BlockType.DEFINITION_ITEM |
| || currentBlockType == BlockType.DEFINITION_TERM) { |
| return new ListBlock(type, 1); |
| } |
| } |
| return new ListBlock(type, 2); |
| case LIST_ITEM: |
| if (computeCurrentListType() == BlockType.NUMERIC_LIST) { |
| return new ListItemBlock("1. "); //$NON-NLS-1$ |
| } |
| return new ListItemBlock("* "); //$NON-NLS-1$ |
| case CODE: |
| return new PrefixedLineContentBlock(type, " ", "", 1, 2); //$NON-NLS-1$ //$NON-NLS-2$ |
| default: |
| Logger.getLogger(getClass().getName()).warning("Unexpected block type: " + type); //$NON-NLS-1$ |
| return new ContentBlock(type, "", "", 2, 2); //$NON-NLS-1$ //$NON-NLS-2$ |
| } |
| } |
| |
| @Override |
| protected Block computeSpan(SpanType type, Attributes attributes) { |
| switch (type) { |
| case LINK: |
| if (attributes instanceof LinkAttributes) { |
| return new LinkBlock((LinkAttributes) attributes); |
| } |
| return new ContentBlock("<", ">", 0, 0); //$NON-NLS-1$ //$NON-NLS-2$ |
| case ITALIC: |
| case EMPHASIS: |
| case MARK: |
| return new ContentBlock("*", "*", 0, 0); //$NON-NLS-1$ //$NON-NLS-2$ |
| case BOLD: |
| case STRONG: |
| return new ContentBlock("**", "**", 0, 0); //$NON-NLS-1$ //$NON-NLS-2$ |
| case CODE: |
| return new CodeSpan(); |
| default: |
| Logger.getLogger(getClass().getName()).warning("Unexpected block type: " + type); //$NON-NLS-1$ |
| return new ContentBlock("", "", 0, 0); //$NON-NLS-1$ //$NON-NLS-2$ |
| } |
| } |
| |
| @Override |
| protected Block computeHeading(int level, Attributes attributes) { |
| return new ContentBlock(computePrefix('#', level) + " ", "", 1, 2); //$NON-NLS-1$ //$NON-NLS-2$ |
| } |
| |
| @Override |
| public void characters(String text) { |
| text = escapeAmpersand(text); |
| assertOpenBlock(); |
| try { |
| currentBlock.write(text); |
| } catch (IOException e) { |
| throw new RuntimeException(e); |
| } |
| } |
| |
| private String escapeAmpersand(String text) { |
| return text.replace("&", "&"); //$NON-NLS-1$ //$NON-NLS-2$ |
| } |
| |
| @Override |
| public void entityReference(String entity) { |
| assertOpenBlock(); |
| String literal = entityToLiteral.get(entity); |
| if (literal == null) { |
| literal = "&" + entity + ";"; //$NON-NLS-1$//$NON-NLS-2$ |
| } |
| try { |
| currentBlock.write(literal); |
| } catch (IOException e) { |
| throw new RuntimeException(e); |
| } |
| } |
| |
| @Override |
| public void image(Attributes attributes, String url) { |
| assertOpenBlock(); |
| try { |
| currentBlock.write(computeImage(attributes, url)); |
| } catch (IOException e) { |
| throw new RuntimeException(e); |
| } |
| } |
| |
| private String computeImage(Attributes attributes, String url) { |
| // ![](/path/to/img.jpg) or |
| // ![alt text](path/to/img.jpg "title") |
| String altText = ""; //$NON-NLS-1$ |
| String title = ""; //$NON-NLS-1$ |
| if (attributes instanceof ImageAttributes) { |
| ImageAttributes imageAttr = (ImageAttributes) attributes; |
| altText = Strings.nullToEmpty(imageAttr.getAlt()); |
| } |
| if (!Strings.isNullOrEmpty(attributes.getTitle())) { |
| title = " \"" + attributes.getTitle() + '"'; //$NON-NLS-1$ |
| } |
| return "![" + altText + "](" + Strings.nullToEmpty(url) + title + ')'; //$NON-NLS-1$ //$NON-NLS-2$ |
| } |
| |
| @Override |
| public void link(Attributes attributes, String hrefOrHashName, String text) { |
| assertOpenBlock(); |
| LinkAttributes linkAttr = new LinkAttributes(); |
| linkAttr.setTitle(attributes.getTitle()); |
| linkAttr.setHref(hrefOrHashName); |
| beginSpan(SpanType.LINK, linkAttr); |
| characters(text); |
| endSpan(); |
| } |
| |
| @Override |
| public void imageLink(Attributes linkAttributes, Attributes imageAttributes, String href, String imageUrl) { |
| link(linkAttributes, href, computeImage(imageAttributes, imageUrl)); |
| } |
| |
| @Override |
| public void acronym(String text, String definition) { |
| throw new UnsupportedOperationException(); |
| } |
| |
| @Override |
| public void lineBreak() { |
| assertOpenBlock(); |
| try { |
| if (currentBlock instanceof MarkdownBlock) { |
| ((MarkdownBlock) currentBlock).lineBreak(); |
| } |
| } catch (IOException e) { |
| throw new RuntimeException(e); |
| } |
| } |
| |
| @Override |
| protected Block createImplicitParagraphBlock() { |
| return new ImplicitParagraphBlock(); |
| } |
| |
| } |