From dc921ce46e04b9d18155b89bc0953fdd4e2426e7 Mon Sep 17 00:00:00 2001
From: Yuri Campolongo <yuri.campolongo@voxage.com.br>
Date: Mon, 9 Apr 2018 14:14:04 -0300
Subject: [PATCH] Automatic Table Of Contents Generator

- Generate a Table of contentes automatically
- just put the tag '__TOC__' in your markdown file and gitblit will automatically generate a table of contents based in your headings
- Make the anchor between the table of contents and the heading using HTML anchor mechanism
---
 .../com/gitblit/markdown/ModifyModel.java     | 55 +++++++++++
 .../com/gitblit/markdown/ParameterUtils.java  | 31 ++++++
 .../markdown/TableOfContentsGenerator.java    | 97 +++++++++++++++++++
 src/main/java/com/gitblit/markdown/Utils.java | 63 ++++++++++++
 .../com/gitblit/utils/JSoupXssFilter.java     |  1 +
 .../com/gitblit/wicket/MarkupProcessor.java   | 29 +++++-
 6 files changed, 275 insertions(+), 1 deletion(-)
 create mode 100644 src/main/java/com/gitblit/markdown/ModifyModel.java
 create mode 100644 src/main/java/com/gitblit/markdown/ParameterUtils.java
 create mode 100644 src/main/java/com/gitblit/markdown/TableOfContentsGenerator.java
 create mode 100644 src/main/java/com/gitblit/markdown/Utils.java

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 = "&nbsp;&nbsp;&nbsp;";
+
+    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<ModifyModel>	rootModels			= new ArrayList<ModifyModel>();
+
+	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<String> fileContent = new ArrayList<String>(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<String> 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>(.*?)<\\/h\\d>");
+		Matcher m = insideHeader.matcher(content);
+
+		while (m.find()) {
+			String id = Utils.normalize(m.group().replaceAll("<h\\d>", "").replaceAll("<\\/h\\d>", "")); 
+			Pattern iniTag = Pattern.compile("<h\\d");
+			Matcher lookTag = iniTag.matcher(m.group());
+			String tag = "";
+			if (lookTag.find()) {
+				tag = lookTag.group();
+			}
+			String finalTag = m.group().replace(tag, tag + " id=\"" + id + "\"");
+
+			content = content.replace(m.group(), finalTag);
+		}
+		return content;
+	}
 
 	private String getWicketUrl(Class<? extends Page> pageClass, final String repositoryName, final String commitId, final String document) {
 		String fsc = settings.getString(Keys.web.forwardSlashCharacter, "/");