Merge "Add IntelliJ project files to .gitignore"
diff --git a/Documentation/markdown.md b/Documentation/markdown.md
index 652d5ed..3e0f392 100644
--- a/Documentation/markdown.md
+++ b/Documentation/markdown.md
@@ -187,13 +187,6 @@
 Note two tildes are required (`~~`) on either side of the struck out
 section of text.
 
-### Smart quotes
-
-'Single' and "double" quotes in paragraph text are
-replaced with smart quotes.  Apostrophes (this doc's text), ellipses
-("...") and dashes ("--" and "---") are also replaced with HTML
-entities to make the documentation appear typeset.
-
 ### Blockquotes
 
 Blockquoted text can be used to stand off text obtained from
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/doc/BlockNoteExtension.java b/gitiles-servlet/src/main/java/com/google/gitiles/doc/BlockNoteExtension.java
index 7c18d60..3ae747d 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/doc/BlockNoteExtension.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/doc/BlockNoteExtension.java
@@ -51,10 +51,12 @@
   }
 
   private static class NoteParser extends AbstractBlockParser {
+    private final int indent;
     private final BlockNote block;
     private boolean done;
 
-    NoteParser(String style) {
+    NoteParser(int indent, String style) {
+      this.indent = indent;
       block = new BlockNote();
       block.setClassName(style);
     }
@@ -69,7 +71,7 @@
       if (done) {
         return BlockContinue.none();
       }
-      if (state.getIndent() == 0) {
+      if (state.getIndent() <= indent) {
         int s = state.getNextNonSpaceIndex();
         CharSequence line = state.getLine();
         if ("***".contentEquals(line.subSequence(s, line.length()))) {
@@ -94,23 +96,19 @@
   private static class NoteParserFactory extends AbstractBlockParserFactory {
     @Override
     public BlockStart tryStart(ParserState state, MatchedBlockParser matched) {
-      if (state.getIndent() > 0) {
-        return BlockStart.none();
-      }
-
       int s = state.getNextNonSpaceIndex();
       CharSequence line = state.getLine();
       CharSequence text = line.subSequence(s, line.length());
-      if (text.length() < 4 || !"*** ".contentEquals(text.subSequence(0, 4))) {
+      if (text.length() < 3 || !"***".contentEquals(text.subSequence(0, 3))) {
         return BlockStart.none();
       }
 
-      String style = text.subSequence(4, text.length()).toString().trim();
+      String style = text.subSequence(3, text.length()).toString().trim();
       if (!VALID_STYLES.contains(style)) {
         return BlockStart.none();
       }
 
-      return BlockStart.of(new NoteParser(style)).atIndex(line.length());
+      return BlockStart.of(new NoteParser(state.getIndent(), style)).atIndex(line.length());
     }
   }
 }
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/doc/MarkdownToHtml.java b/gitiles-servlet/src/main/java/com/google/gitiles/doc/MarkdownToHtml.java
index 7a116c1..d5218e7 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/doc/MarkdownToHtml.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/doc/MarkdownToHtml.java
@@ -61,8 +61,6 @@
 import org.eclipse.jgit.revwalk.RevTree;
 
 import java.util.List;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
 
 import javax.annotation.Nullable;
 
@@ -366,6 +364,11 @@
   String href(String target) {
     if (target.startsWith("#") || HtmlBuilder.isValidHttpUri(target)) {
       return target;
+    } else if (target.startsWith("git:")) {
+      if (HtmlBuilder.isValidGitUri(target)) {
+        return target;
+      }
+      return FilterNormalizeUri.INSTANCE.getInnocuousOutput();
     }
 
     String anchor = "";
@@ -457,37 +460,9 @@
     }
   }
 
-  private static final Pattern PRETTY = Pattern.compile("('|[.]{3}|-{2,3})");
-
   @Override
   public void visit(Text node) {
-    String text = node.getLiteral();
-    Matcher pretty = PRETTY.matcher(text);
-    int i = 0;
-    while (pretty.find()) {
-      int s = pretty.start();
-      if (i < s) {
-        html.appendAndEscape(text.substring(i, s));
-      }
-      switch (pretty.group(0)) {
-        case "'":
-          html.entity("&rsquo;");
-          break;
-        case "...":
-          html.entity("&hellip;");
-          break;
-        case "--":
-          html.entity("&ndash;");
-          break;
-        case "---":
-          html.entity("&mdash;");
-          break;
-      }
-      i = pretty.end();
-    }
-    if (i < text.length()) {
-      html.appendAndEscape(text.substring(i));
-    }
+    html.appendAndEscape(node.getLiteral());
   }
 
   @Override
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/doc/MultiColumnExtension.java b/gitiles-servlet/src/main/java/com/google/gitiles/doc/MultiColumnExtension.java
index ef53fe3..aeb3d89 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/doc/MultiColumnExtension.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/doc/MultiColumnExtension.java
@@ -46,7 +46,7 @@
 
   @Override
   public void extend(Parser.Builder builder) {
-    builder.customBlockParserFactory(new DivParserFactory());
+    builder.customBlockParserFactory(new MultiColumnParserFactory());
   }
 
   private static class MultiColumnParser extends AbstractBlockParser {
@@ -151,7 +151,7 @@
     }
   }
 
-  private static class DivParserFactory extends AbstractBlockParserFactory {
+  private static class MultiColumnParserFactory extends AbstractBlockParserFactory {
     @Override
     public BlockStart tryStart(ParserState state, MatchedBlockParser matched) {
       if (state.getIndent() > 0) {
@@ -161,12 +161,13 @@
       int s = state.getNextNonSpaceIndex();
       CharSequence line = state.getLine();
       CharSequence text = line.subSequence(s, line.length());
-      if (text.length() >= MARKER.length()
-          && MARKER.contentEquals(text.subSequence(0, MARKER.length()))) {
-        String layout = text.subSequence(MARKER.length(), line.length()).toString().trim();
-        return BlockStart.of(new MultiColumnParser(layout)).atIndex(line.length());
+      if (text.length() < MARKER.length()
+          || !MARKER.contentEquals(text.subSequence(0, MARKER.length()))) {
+        return BlockStart.none();
       }
-      return BlockStart.none();
+
+      String layout = text.subSequence(MARKER.length(), text.length()).toString().trim();
+      return BlockStart.of(new MultiColumnParser(layout)).atIndex(text.length());
     }
   }
 }
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/doc/html/HtmlBuilder.java b/gitiles-servlet/src/main/java/com/google/gitiles/doc/html/HtmlBuilder.java
index 9639176..3239fe1 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/doc/html/HtmlBuilder.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/doc/html/HtmlBuilder.java
@@ -85,6 +85,17 @@
   private static final FilterNormalizeUri URI = FilterNormalizeUri.INSTANCE;
   private static final FilterImageDataUri IMAGE_DATA = FilterImageDataUri.INSTANCE;
 
+  private static final Pattern GIT_URI =
+      Pattern.compile(
+          "^"
+              +
+              // Reject paths containing /../ or ending in /..
+              "(?![^#?]*/(?:\\.|%2E){2}(?:[/?#]|\\z))"
+              +
+              // Accept git://host/path
+              "git://[^/]+/.+",
+          Pattern.CASE_INSENSITIVE);
+
   public static boolean isValidCssDimension(String val) {
     return val != null && val.matches("(?:[1-9][0-9]*px|100%|[1-9][0-9]?%)");
   }
@@ -99,6 +110,10 @@
     return IMAGE_DATA.getValueFilter().matcher(url).find();
   }
 
+  public static boolean isValidGitUri(String val) {
+    return GIT_URI.matcher(val).find();
+  }
+
   private final StringBuilder htmlBuf;
   private final Appendable textBuf;
   private String tag;
@@ -159,7 +174,7 @@
   }
 
   private String anchorHref(String val) {
-    if (URI.getValueFilter().matcher(val).find()) {
+    if (URI.getValueFilter().matcher(val).find() || isValidGitUri(val)) {
       return URI.escape(val);
     }
     return URI.getInnocuousOutput();
diff --git a/gitiles-servlet/src/test/java/com/google/gitiles/doc/DocServletTest.java b/gitiles-servlet/src/test/java/com/google/gitiles/doc/DocServletTest.java
index f73c331..25f3d7f 100644
--- a/gitiles-servlet/src/test/java/com/google/gitiles/doc/DocServletTest.java
+++ b/gitiles-servlet/src/test/java/com/google/gitiles/doc/DocServletTest.java
@@ -104,6 +104,20 @@
   }
 
   @Test
+  public void noteInList() throws Exception {
+    String markdown =
+        "+ one\n\n" + "    ***aside\n" + "    remember this\n" + "    ***\n" + "\n" + "+ two\n";
+    repo.branch("master").commit().add("index.md", markdown).create();
+
+    String html = buildHtml("/repo/+/master/index.md");
+    System.out.println(html);
+    assertThat(html)
+        .contains(
+            "<ul><li><p>one</p><div class=\"aside\">remember this</div>"
+                + "</li><li><p>two</p></li></ul>");
+  }
+
+  @Test
   public void relativeLink() throws Exception {
     repo.branch("master").commit().add("A/B/README.md", "[c](../../C)").create();
 
@@ -118,4 +132,20 @@
     String html = buildHtml("/repo/+doc/master/README.md");
     assertThat(html).contains("<a href=\"/b/repo/+/master/x\">c</a>");
   }
+
+  @Test
+  public void gitUrlLink() throws Exception {
+    repo.branch("master").commit().add("README.md", "[c](git://example.com/repo.git)").create();
+
+    String html = buildHtml("/repo/+doc/master/README.md");
+    assertThat(html).contains("<a href=\"git://example.com/repo.git\">c</a>");
+  }
+
+  @Test
+  public void invalidGitUrlLink() throws Exception {
+    repo.branch("master").commit().add("README.md", "[c](git://example.com/repo/..)").create();
+
+    String html = buildHtml("/repo/+doc/master/README.md");
+    assertThat(html).contains("<a href=\"#zSoyz\">c</a>");
+  }
 }
diff --git a/gitiles-servlet/src/test/java/com/google/gitiles/doc/LinkTest.java b/gitiles-servlet/src/test/java/com/google/gitiles/doc/LinkTest.java
index fdffffc..ad9f941 100644
--- a/gitiles-servlet/src/test/java/com/google/gitiles/doc/LinkTest.java
+++ b/gitiles-servlet/src/test/java/com/google/gitiles/doc/LinkTest.java
@@ -63,6 +63,25 @@
   }
 
   @Test
+  public void gitLink() {
+    MarkdownToHtml md =
+        MarkdownToHtml.builder()
+            .setGitilesView(view)
+            .setConfig(new MarkdownConfig(config))
+            .setFilePath("index.md")
+            .build();
+    String url;
+
+    url = "git://example.com/repo.git";
+    assertThat(md.href(url)).isEqualTo(url);
+
+    assertThat(md.href("git:example.com/repo.git")).isEqualTo("#zSoyz");
+    assertThat(md.href("git://")).isEqualTo("#zSoyz");
+    assertThat(md.href("git://example.com/../root")).isEqualTo("#zSoyz");
+    assertThat(md.href("git://example.com/root/..")).isEqualTo("#zSoyz");
+  }
+
+  @Test
   public void absolutePath() {
     MarkdownToHtml md =
         MarkdownToHtml.builder()