| package com.intellij.util.text; |
| |
| import com.intellij.openapi.util.TextRange; |
| import com.intellij.openapi.util.text.StringUtil; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| /** |
| * @author Sergey Simonchik |
| */ |
| public class MarkdownUtil { |
| |
| private MarkdownUtil() {} |
| |
| /** |
| * Replaces headers in markdown with HTML. |
| * Unfortunately, library used for markdown processing |
| * <a href="https://code.google.com/p/markdownj">markdownj</a> |
| * doesn't support that. |
| */ |
| public static void replaceHeaders(@NotNull List<String> lines) { |
| for (int i = 0; i < lines.size(); i++) { |
| String line = lines.get(i); |
| int ind = 0; |
| while (ind < line.length() && line.charAt(ind) == '#') { |
| ind++; |
| } |
| if (ind < line.length() && line.charAt(ind) == ' ') { |
| if (0 < ind && ind <= 9) { |
| int endInd = line.length() - 1; |
| while (endInd >= 0 && line.charAt(endInd) == '#') { |
| endInd--; |
| } |
| line = line.substring(ind + 1, endInd + 1); |
| line = "<h" + ind + ">" + line + "</h" + ind + ">"; |
| lines.set(i, line); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Removes images in the markdown text. |
| * @param lines List of String in markdown format |
| */ |
| public static void removeImages(@NotNull List<String> lines) { |
| for (int i = 0; i < lines.size(); i++) { |
| String newText = removeAllImages(lines.get(i)); |
| lines.set(i, newText); |
| } |
| } |
| |
| @NotNull |
| private static String removeAllImages(@NotNull String text) { |
| int n = text.length(); |
| List<TextRange> intervals = null; |
| int i = 0; |
| while (i < n) { |
| int imageEndIndex = findImageEndIndexInclusive(text, i); |
| if (imageEndIndex != -1) { |
| TextRange linkRange = findEnclosingLink(text, i, imageEndIndex); |
| if (intervals == null) { |
| intervals = new ArrayList<TextRange>(1); |
| } |
| final TextRange range; |
| if (linkRange != null) { |
| range = linkRange; |
| } |
| else { |
| range = new TextRange(i, imageEndIndex); |
| } |
| intervals.add(range); |
| i = range.getEndOffset(); |
| } |
| i++; |
| } |
| if (intervals == null) { |
| return text; |
| } |
| StringBuilder buf = new StringBuilder(text); |
| for (int intervalInd = intervals.size() - 1; intervalInd >= 0; intervalInd--) { |
| TextRange range = intervals.get(intervalInd); |
| buf.delete(range.getStartOffset(), range.getEndOffset() + 1); |
| } |
| return buf.toString(); |
| } |
| |
| private static int findImageEndIndexInclusive(@NotNull String text, int imageStartIndex) { |
| int n = text.length(); |
| if (text.charAt(imageStartIndex) == '!' |
| && imageStartIndex + 1 < n |
| && text.charAt(imageStartIndex + 1) == '[') { |
| int i = imageStartIndex + 2; |
| while (i < n && text.charAt(i) != ']') { |
| i++; |
| } |
| if (i < n && i + 1 < n && text.charAt(i + 1) == '(') { |
| i += 2; |
| while (i < n && text.charAt(i) != ')') { |
| i++; |
| } |
| if (i < n) { |
| return i; |
| } |
| } |
| } |
| return -1; |
| } |
| |
| @Nullable |
| private static TextRange findEnclosingLink(@NotNull String text, |
| int imageStartIndInc, |
| int imageEndIndInc) { |
| int linkStartIndInc = imageStartIndInc - 1; |
| if (linkStartIndInc >= 0 && text.charAt(linkStartIndInc) == '[') { |
| int n = text.length(); |
| int i = imageEndIndInc + 1; |
| if (text.charAt(i) == ']' && i + 1 < n && text.charAt(i + 1) == '(') { |
| i += 2; |
| while (i < n && text.charAt(i) != ')') { |
| i++; |
| } |
| if (i < n) { |
| return new TextRange(linkStartIndInc, i); |
| } |
| } |
| } |
| return null; |
| } |
| |
| public static void replaceCodeBlock(@NotNull List<String> lines) { |
| new CodeBlockProcessor(lines).process(); |
| } |
| |
| private static class CodeBlockProcessor { |
| |
| private static final String START_TAGS = "<pre><code>"; |
| private static final String END_TAGS = "</code></pre>"; |
| |
| private final List<String> myLines; |
| |
| private boolean myGlobalCodeBlockStarted = false; |
| private boolean myCodeBlockStarted = false; |
| |
| private CodeBlockProcessor(@NotNull List<String> lines) { |
| myLines = lines; |
| } |
| |
| public void process() { |
| for (int i = 0; i < myLines.size(); i++) { |
| final String line = myLines.get(i); |
| if (line.startsWith("```")) { |
| finishCodeBlock(i - 1); |
| myGlobalCodeBlockStarted = !myGlobalCodeBlockStarted; |
| String out = myGlobalCodeBlockStarted ? START_TAGS : END_TAGS; |
| myLines.set(i, out); |
| } |
| else { |
| if (!myGlobalCodeBlockStarted) { |
| handleLocalCodeBlock(i, line); |
| } |
| } |
| } |
| finishCodeBlock(myLines.size() - 1); |
| } |
| |
| private void handleLocalCodeBlock(int ind, @NotNull String line) { |
| boolean codeBlock = false; |
| if (line.startsWith(" ")) { |
| line = line.substring(4); |
| codeBlock = true; |
| } |
| else if (line.startsWith("\t")) { |
| line = line.substring(1); |
| codeBlock = true; |
| } |
| |
| if (!myCodeBlockStarted) { |
| if (codeBlock) { |
| myCodeBlockStarted = true; |
| myLines.set(ind, START_TAGS + line); |
| } |
| } |
| else { |
| if (codeBlock) { |
| myLines.set(ind, line); |
| } |
| else { |
| finishCodeBlock(ind - 1); |
| } |
| } |
| } |
| |
| private void finishCodeBlock(int lastCodeBlockLineInd) { |
| if (myCodeBlockStarted) { |
| myLines.set(lastCodeBlockLineInd, myLines.get(lastCodeBlockLineInd) + END_TAGS); |
| myCodeBlockStarted = false; |
| } |
| } |
| } |
| |
| public static void generateLists(@NotNull List<String> lines) { |
| new ListItemProcessor(lines).process(); |
| } |
| |
| private static class ListItemProcessor { |
| |
| private final List<String> myLines; |
| |
| private boolean myInsideBlockQuote = false; |
| private ListItem myFirstListItem = null; |
| private int myLastListItemLineInd = -1; |
| |
| private ListItemProcessor(@NotNull List<String> lines) { |
| myLines = lines; |
| } |
| |
| public void process() { |
| for (int i = 0; i < myLines.size(); i++) { |
| final String line = myLines.get(i); |
| if (line.startsWith("```")) { |
| myInsideBlockQuote = !myInsideBlockQuote; |
| } |
| if (!myInsideBlockQuote) { |
| handle(i, line); |
| } |
| } |
| finishLastListItem(true); |
| } |
| |
| private void handle(int ind, @NotNull String line) { |
| ListItem listItem = toListItem(line); |
| if (listItem != null) { |
| finishLastListItem(false); |
| String out = "<li>" + listItem.getBody(); |
| if (myFirstListItem == null) { |
| myFirstListItem = listItem; |
| if (listItem.isUnordered()) { |
| out = "<ul>" + out; |
| } |
| else { |
| out = "<ol>" + out; |
| } |
| } |
| myLines.set(ind, out); |
| myLastListItemLineInd = ind; |
| } |
| else if (myFirstListItem != null && |
| !line.isEmpty() && |
| !StringUtil.isEmptyOrSpaces(line)) { |
| if (ind - 1 >= 0 |
| && StringUtil.isEmptyOrSpaces(myLines.get(ind - 1)) |
| && !Character.isWhitespace(line.charAt(0))) { |
| finishLastListItem(true); |
| } |
| else { |
| String m = StringUtil.trimLeading(line); |
| myLines.set(ind, m); |
| myLastListItemLineInd = ind; |
| } |
| } |
| } |
| |
| private void finishLastListItem(boolean finishList) { |
| if (myLastListItemLineInd != -1) { |
| String l = myLines.get(myLastListItemLineInd); |
| l += "</li>"; |
| if (finishList) { |
| if (myFirstListItem.isUnordered()) { |
| l += "</ul>"; |
| } |
| else { |
| l += "</ol>"; |
| } |
| myFirstListItem = null; |
| } |
| myLines.set(myLastListItemLineInd, l); |
| myLastListItemLineInd = -1; |
| } |
| } |
| } |
| |
| @Nullable |
| private static ListItem toListItem(@NotNull String line) { |
| line = StringUtil.trimLeading(line); |
| if (line.length() >= 2) { |
| char firstChar = line.charAt(0); |
| char secondChar = line.charAt(1); |
| if (firstChar == '*' || firstChar == '+' || firstChar == '-') { |
| if (Character.isWhitespace(secondChar)) { |
| return new ListItem(true, StringUtil.trimLeading(line.substring(1))); |
| } |
| } |
| } |
| int i = 0; |
| while (i < line.length() && Character.isDigit(line.charAt(i))) { |
| i++; |
| } |
| if (i > 0 && i < line.length() - 1) { |
| if (line.charAt(i) == '.' && Character.isWhitespace(line.charAt(i + 1))) { |
| return new ListItem(false, StringUtil.trimLeading(line.substring(i + 1))); |
| } |
| } |
| return null; |
| } |
| |
| private static class ListItem { |
| |
| private final boolean myUnordered; |
| private final String myBody; |
| |
| private ListItem(boolean unordered, @NotNull String body) { |
| myUnordered = unordered; |
| myBody = body; |
| } |
| |
| private boolean isUnordered() { |
| return myUnordered; |
| } |
| |
| @NotNull |
| private String getBody() { |
| return myBody; |
| } |
| } |
| |
| } |