diff --git a/src/main/java/com/gitblit/markdown/ModifyModel.java b/src/main/java/com/gitblit/markdown/ModifyModel.java new file mode 100644 index 000000000..f446f0960 --- /dev/null +++ b/src/main/java/com/gitblit/markdown/ModifyModel.java @@ -0,0 +1,55 @@ +package com.gitblit.markdown; + +/** + * Created by Yuriy Aizenberg + */ +public class ModifyModel { + + private static final String PATTERN = "%s- [%s](#%s)"; + private static final String AFFIX = "   "; + + private int currentDeepLevel = 1; + private String headerName; + private String headerLink; + + + public ModifyModel(int currentDeepLevel, String headerName, String headerLink) { + this.currentDeepLevel = currentDeepLevel; + this.headerName = headerName; + this.headerLink = headerLink; + } + + public int getCurrentDeepLevel() { + return currentDeepLevel; + } + + public void setCurrentDeepLevel(int currentDeepLevel) { + this.currentDeepLevel = currentDeepLevel; + } + + public String getHeaderName() { + return headerName; + } + + public void setHeaderName(String headerName) { + this.headerName = headerName; + } + + public String getHeaderLink() { + return headerLink; + } + + public void setHeaderLink(String headerLink) { + this.headerLink = headerLink; + } + + public String create() { + String affixs = ""; + if (currentDeepLevel > 1) { + for (int i = 0; i < currentDeepLevel - 1; i++) { + affixs += AFFIX; + } + } + return String.format(PATTERN, affixs, headerName, headerLink); + } +} diff --git a/src/main/java/com/gitblit/markdown/ParameterUtils.java b/src/main/java/com/gitblit/markdown/ParameterUtils.java new file mode 100644 index 000000000..281c46c4a --- /dev/null +++ b/src/main/java/com/gitblit/markdown/ParameterUtils.java @@ -0,0 +1,31 @@ +package com.gitblit.markdown; + +/** + * Created by Yuriy Aizenberg + */ +public class ParameterUtils { + + public static Integer extractInteger(String key, Integer defValue) { + if (Utils.isEmpty(key)) return defValue; + try { + return Integer.valueOf(key); + } catch (NumberFormatException e) { + return defValue; + } + } + + public static Integer extractInteger(String key) { + return extractInteger(key, null); + } + + public static Boolean extractBoolean(String key, Boolean defValue) { + if (Utils.isEmpty(key)) return defValue; + return Boolean.valueOf(key); + } + + public static Boolean extractBoolean(String key) { + return extractBoolean(key, null); + } + + +} diff --git a/src/main/java/com/gitblit/markdown/TableOfContentsGenerator.java b/src/main/java/com/gitblit/markdown/TableOfContentsGenerator.java new file mode 100644 index 000000000..7b3efbada --- /dev/null +++ b/src/main/java/com/gitblit/markdown/TableOfContentsGenerator.java @@ -0,0 +1,97 @@ +package com.gitblit.markdown; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class TableOfContentsGenerator { + + public static final String DEFAULT_HEADER = "#"; + public static final char DEFAULT_HEADER_CHAR = '#'; + public static final String ALT_1_HEADER = "="; + public static final String ALT_2_HEADER = "-"; + + public static final String TOC_HEADER = "## Table of contents"; + + private String markdown; + private List rootModels = new ArrayList(); + + public TableOfContentsGenerator(String markdown) { + this.markdown = markdown; + } + + public String start() { + + if (!this.markdown.contains("__TOC__")) { + return this.markdown; + } + + int patternLineNumber = -1; + int currentLine = 0; + String[] items = markdown.split("\n"); + List fileContent = new ArrayList(Arrays.asList(items)); + + String previousLine = null; + + for (String line : fileContent) { + ++currentLine; + if (line.startsWith(DEFAULT_HEADER)) { + String trim = line.trim(); + + int count = getCount(trim); + if (count < 1 || count > 6) { + previousLine = line; + continue; + } + String headerName = line.substring(count); + rootModels.add(new ModifyModel(count, headerName, Utils.normalize(headerName))); + } else if (line.startsWith(ALT_1_HEADER) && !Utils.isEmpty(previousLine)) { + if (line.replaceAll(ALT_1_HEADER, "").isEmpty()) { + rootModels.add(new ModifyModel(1, previousLine, Utils.normalize(previousLine))); + } + } else if (line.startsWith(ALT_2_HEADER) && !Utils.isEmpty(previousLine)) { + if (line.replaceAll(ALT_2_HEADER, "").isEmpty()) { + rootModels.add(new ModifyModel(2, previousLine, Utils.normalize(previousLine))); + } + } else if (line.trim().equals("__TOC__")) { + patternLineNumber = currentLine; + } + previousLine = line; + } + + return getMarkdown(fileContent, patternLineNumber).replaceAll("__TOC__", ""); + + } + + private String getMarkdown(List fileContent, int patternLineNumber) { + StringBuilder writer = new StringBuilder(); + if (patternLineNumber == -1) { + System.out.println("Pattern for replace not found!"); + return ""; + } + fileContent.add(patternLineNumber - 1, TOC_HEADER); + for (ModifyModel modifyModel : rootModels) { + fileContent.add(patternLineNumber, modifyModel.create()); + patternLineNumber++; + } + + for (String line : fileContent) { + writer.append(line).append("\n"); + } + + return writer.toString(); + } + + private int getCount(String string) { + int count = 0; + for (int i = 0; i < string.length(); i++) { + if (string.charAt(i) == DEFAULT_HEADER_CHAR) { + ++count; + } else { + break; + } + } + return count; + } + +} diff --git a/src/main/java/com/gitblit/markdown/Utils.java b/src/main/java/com/gitblit/markdown/Utils.java new file mode 100644 index 000000000..b10470b53 --- /dev/null +++ b/src/main/java/com/gitblit/markdown/Utils.java @@ -0,0 +1,63 @@ +package com.gitblit.markdown; + +import java.io.*; + +/** + * Created by Yuriy Aizenberg + */ +public class Utils { + + private static final String SPACES = " "; + private static final String CODES = "%([abcdef]|\\d){2,2}"; + private static final String SPECIAL_CHARS = "[\\/?!:\\[\\]`.,()*\"';{}+=<>~\\$|#]"; + private static final String DASH = "-"; + private static final String EMPTY = ""; + + + public static boolean checkSourceFile(String fileName) { + if (isEmpty(fileName)) return false; + File sourceFile = new File(fileName); + return sourceFile.exists() && sourceFile.canRead() && sourceFile.isFile(); + } + + public static boolean isEmpty(String string) { + return string == null || string.isEmpty(); + } + + public static int getFileLines(String filePath, int defFaultValue) { + LineNumberReader lineNumberReader = null; + FileReader fileReader = null; + try { + fileReader = new FileReader(filePath); + lineNumberReader = new LineNumberReader(fileReader); + lineNumberReader.skip(Long.MAX_VALUE); + return lineNumberReader.getLineNumber() + 1; + } catch (IOException ignored) { + } finally { + closeStream(lineNumberReader, fileReader); + } + return defFaultValue; + } + + public static void closeStream(Closeable... closeable) { + for (Closeable c : closeable) { + if (c != null) { + try { + c.close(); + } catch (IOException ignored) { + } + } + } + } + + public static String normalize(final String taintedURL) { + return taintedURL + .trim() + + .replaceAll(SPACES, DASH) + + .replaceAll(CODES, EMPTY) + + .replaceAll(SPECIAL_CHARS, EMPTY).toLowerCase(); + } +} diff --git a/src/main/java/com/gitblit/utils/JSoupXssFilter.java b/src/main/java/com/gitblit/utils/JSoupXssFilter.java index aec22411a..2e1e4fac2 100644 --- a/src/main/java/com/gitblit/utils/JSoupXssFilter.java +++ b/src/main/java/com/gitblit/utils/JSoupXssFilter.java @@ -81,6 +81,7 @@ protected Whitelist getRelaxedWhiteList() { .addAttributes("img", "align", "alt", "height", "src", "title", "width") .addAttributes("ol", "start", "type") .addAttributes("q", "cite") + .addAttributes(":all", "id") .addAttributes("span", "class", "style") .addAttributes("table", "class", "style", "summary", "width") .addAttributes("td", "abbr", "axis", "class", "colspan", "rowspan", "style", "width") diff --git a/src/main/java/com/gitblit/wicket/MarkupProcessor.java b/src/main/java/com/gitblit/wicket/MarkupProcessor.java index b20320499..fd110856e 100644 --- a/src/main/java/com/gitblit/wicket/MarkupProcessor.java +++ b/src/main/java/com/gitblit/wicket/MarkupProcessor.java @@ -28,6 +28,8 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.apache.wicket.Page; import org.apache.wicket.RequestCycle; @@ -55,6 +57,8 @@ import com.gitblit.IStoredSettings; import com.gitblit.Keys; +import com.gitblit.markdown.TableOfContentsGenerator; +import com.gitblit.markdown.Utils; import com.gitblit.models.PathModel; import com.gitblit.servlet.RawServlet; import com.gitblit.utils.JGitUtils; @@ -358,11 +362,34 @@ public Rendering render(WikiLinkNode node) { } }; - final String content = MarkdownUtils.transformMarkdown(doc.markup, renderer); + String markdown = new TableOfContentsGenerator(doc.markup).start(); + String content = MarkdownUtils.transformMarkdown(markdown, renderer); + + content = generateHeaderIds(content); + final String safeContent = xssFilter.relaxed(content); doc.html = safeContent; } + + private String generateHeaderIds(String content) { + Pattern insideHeader = Pattern.compile("(.*?)<\\/h\\d>"); + Matcher m = insideHeader.matcher(content); + + while (m.find()) { + String id = Utils.normalize(m.group().replaceAll("", "").replaceAll("<\\/h\\d>", "")); + Pattern iniTag = Pattern.compile(" pageClass, final String repositoryName, final String commitId, final String document) { String fsc = settings.getString(Keys.web.forwardSlashCharacter, "/");