您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

MarkupProcessor.java 10KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. /*
  2. * Copyright 2013 gitblit.com.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. package com.gitblit.wicket;
  17. import java.io.Serializable;
  18. import java.io.StringWriter;
  19. import java.io.UnsupportedEncodingException;
  20. import java.net.URLEncoder;
  21. import java.text.MessageFormat;
  22. import java.util.ArrayList;
  23. import java.util.List;
  24. import org.apache.wicket.Page;
  25. import org.apache.wicket.RequestCycle;
  26. import org.eclipse.jgit.lib.Repository;
  27. import org.eclipse.jgit.revwalk.RevCommit;
  28. import org.eclipse.mylyn.wikitext.confluence.core.ConfluenceLanguage;
  29. import org.eclipse.mylyn.wikitext.core.parser.Attributes;
  30. import org.eclipse.mylyn.wikitext.core.parser.MarkupParser;
  31. import org.eclipse.mylyn.wikitext.core.parser.builder.HtmlDocumentBuilder;
  32. import org.eclipse.mylyn.wikitext.core.parser.markup.MarkupLanguage;
  33. import org.eclipse.mylyn.wikitext.mediawiki.core.MediaWikiLanguage;
  34. import org.eclipse.mylyn.wikitext.textile.core.TextileLanguage;
  35. import org.eclipse.mylyn.wikitext.tracwiki.core.TracWikiLanguage;
  36. import org.eclipse.mylyn.wikitext.twiki.core.TWikiLanguage;
  37. import org.pegdown.LinkRenderer;
  38. import org.pegdown.ast.WikiLinkNode;
  39. import org.slf4j.Logger;
  40. import org.slf4j.LoggerFactory;
  41. import com.gitblit.IStoredSettings;
  42. import com.gitblit.Keys;
  43. import com.gitblit.models.PathModel;
  44. import com.gitblit.utils.JGitUtils;
  45. import com.gitblit.utils.MarkdownUtils;
  46. import com.gitblit.utils.StringUtils;
  47. import com.gitblit.wicket.pages.DocPage;
  48. import com.gitblit.wicket.pages.RawPage;
  49. /**
  50. * Processes markup content and generates html with repository-relative page and
  51. * image linking.
  52. *
  53. * @author James Moger
  54. *
  55. */
  56. public class MarkupProcessor {
  57. public enum MarkupSyntax {
  58. PLAIN, MARKDOWN, TWIKI, TRACWIKI, TEXTILE, MEDIAWIKI, CONFLUENCE
  59. }
  60. private Logger logger = LoggerFactory.getLogger(getClass());
  61. private final IStoredSettings settings;
  62. public MarkupProcessor(IStoredSettings settings) {
  63. this.settings = settings;
  64. }
  65. public List<String> getMarkupExtensions() {
  66. List<String> list = new ArrayList<String>();
  67. list.addAll(settings.getStrings(Keys.web.confluenceExtensions));
  68. list.addAll(settings.getStrings(Keys.web.markdownExtensions));
  69. list.addAll(settings.getStrings(Keys.web.mediawikiExtensions));
  70. list.addAll(settings.getStrings(Keys.web.textileExtensions));
  71. list.addAll(settings.getStrings(Keys.web.tracwikiExtensions));
  72. list.addAll(settings.getStrings(Keys.web.twikiExtensions));
  73. return list;
  74. }
  75. private MarkupSyntax determineSyntax(String documentPath) {
  76. String ext = StringUtils.getFileExtension(documentPath).toLowerCase();
  77. if (StringUtils.isEmpty(ext)) {
  78. return MarkupSyntax.PLAIN;
  79. }
  80. if (settings.getStrings(Keys.web.confluenceExtensions).contains(ext)) {
  81. return MarkupSyntax.CONFLUENCE;
  82. } else if (settings.getStrings(Keys.web.markdownExtensions).contains(ext)) {
  83. return MarkupSyntax.MARKDOWN;
  84. } else if (settings.getStrings(Keys.web.mediawikiExtensions).contains(ext)) {
  85. return MarkupSyntax.MEDIAWIKI;
  86. } else if (settings.getStrings(Keys.web.textileExtensions).contains(ext)) {
  87. return MarkupSyntax.TEXTILE;
  88. } else if (settings.getStrings(Keys.web.tracwikiExtensions).contains(ext)) {
  89. return MarkupSyntax.TRACWIKI;
  90. } else if (settings.getStrings(Keys.web.twikiExtensions).contains(ext)) {
  91. return MarkupSyntax.TWIKI;
  92. }
  93. return MarkupSyntax.PLAIN;
  94. }
  95. public MarkupDocument parseReadme(Repository r, String repositoryName, String commitId) {
  96. String readme = null;
  97. RevCommit commit = JGitUtils.getCommit(r, commitId);
  98. List<PathModel> paths = JGitUtils.getFilesInPath(r, null, commit);
  99. for (PathModel path : paths) {
  100. if (!path.isTree()) {
  101. String name = path.name.toLowerCase();
  102. if (name.equals("readme") || name.equals("readme.txt")) {
  103. readme = path.name;
  104. break;
  105. } else if (name.startsWith("readme.")) {
  106. String ext = StringUtils.getFileExtension(name).toLowerCase();
  107. if (getMarkupExtensions().contains(ext)) {
  108. readme = path.name;
  109. break;
  110. }
  111. }
  112. }
  113. }
  114. if (!StringUtils.isEmpty(readme)) {
  115. String [] encodings = settings.getStrings(Keys.web.blobEncodings).toArray(new String[0]);
  116. String markup = JGitUtils.getStringContent(r, commit.getTree(), readme, encodings);
  117. return parse(repositoryName, commitId, readme, markup);
  118. }
  119. return null;
  120. }
  121. public MarkupDocument parse(String repositoryName, String commitId, String documentPath, String markupText) {
  122. final MarkupSyntax syntax = determineSyntax(documentPath);
  123. final MarkupDocument doc = new MarkupDocument(documentPath, markupText, syntax);
  124. if (markupText != null) {
  125. try {
  126. switch (syntax){
  127. case CONFLUENCE:
  128. parse(doc, repositoryName, commitId, new ConfluenceLanguage());
  129. break;
  130. case MARKDOWN:
  131. parse(doc, repositoryName, commitId);
  132. break;
  133. case MEDIAWIKI:
  134. parse(doc, repositoryName, commitId, new MediaWikiLanguage());
  135. break;
  136. case TEXTILE:
  137. parse(doc, repositoryName, commitId, new TextileLanguage());
  138. break;
  139. case TRACWIKI:
  140. parse(doc, repositoryName, commitId, new TracWikiLanguage());
  141. break;
  142. case TWIKI:
  143. parse(doc, repositoryName, commitId, new TWikiLanguage());
  144. break;
  145. default:
  146. doc.html = MarkdownUtils.transformPlainText(markupText);
  147. break;
  148. }
  149. } catch (Exception e) {
  150. logger.error("failed to transform " + syntax, e);
  151. }
  152. }
  153. if (doc.html == null) {
  154. // failed to transform markup
  155. if (markupText == null) {
  156. markupText = String.format("Document <b>%1$s</b> not found in <em>%2$s</em>", documentPath, repositoryName);
  157. }
  158. markupText = MessageFormat.format("<div class=\"alert alert-error\"><strong>{0}:</strong> {1}</div>{2}", "Error", "failed to parse markup", markupText);
  159. doc.html = StringUtils.breakLinesForHtml(markupText);
  160. }
  161. return doc;
  162. }
  163. /**
  164. * Parses the markup using the specified markup language
  165. *
  166. * @param doc
  167. * @param repositoryName
  168. * @param commitId
  169. * @param lang
  170. */
  171. private void parse(final MarkupDocument doc, final String repositoryName, final String commitId, MarkupLanguage lang) {
  172. StringWriter writer = new StringWriter();
  173. HtmlDocumentBuilder builder = new HtmlDocumentBuilder(writer) {
  174. @Override
  175. public void image(Attributes attributes, String imagePath) {
  176. String url;
  177. if (imagePath.indexOf("://") == -1) {
  178. // relative image
  179. String path = doc.getRelativePath(imagePath);
  180. url = getWicketUrl(RawPage.class, repositoryName, commitId, path);
  181. } else {
  182. // absolute image
  183. url = imagePath;
  184. }
  185. super.image(attributes, url);
  186. }
  187. @Override
  188. public void link(Attributes attributes, String hrefOrHashName, String text) {
  189. String url;
  190. if (hrefOrHashName.charAt(0) != '#') {
  191. if (hrefOrHashName.indexOf("://") == -1) {
  192. // relative link
  193. String path = doc.getRelativePath(hrefOrHashName);
  194. url = getWicketUrl(DocPage.class, repositoryName, commitId, path);
  195. } else {
  196. // absolute link
  197. url = hrefOrHashName;
  198. }
  199. } else {
  200. // page-relative hash link
  201. url = hrefOrHashName;
  202. }
  203. super.link(attributes, url, text);
  204. }
  205. };
  206. // avoid the <html> and <body> tags
  207. builder.setEmitAsDocument(false);
  208. MarkupParser parser = new MarkupParser(lang);
  209. parser.setBuilder(builder);
  210. parser.parse(doc.markup);
  211. doc.html = writer.toString();
  212. }
  213. /**
  214. * Parses the document as Markdown using Pegdown.
  215. *
  216. * @param doc
  217. * @param repositoryName
  218. * @param commitId
  219. */
  220. private void parse(final MarkupDocument doc, final String repositoryName, final String commitId) {
  221. LinkRenderer renderer = new LinkRenderer() {
  222. @Override
  223. public Rendering render(WikiLinkNode node) {
  224. String path = doc.getRelativePath(node.getText());
  225. String name = getDocumentName(path);
  226. String url = getWicketUrl(DocPage.class, repositoryName, commitId, path);
  227. return new Rendering(url, name);
  228. }
  229. };
  230. doc.html = MarkdownUtils.transformMarkdown(doc.markup, renderer);
  231. }
  232. private String getWicketUrl(Class<? extends Page> pageClass, final String repositoryName, final String commitId, final String document) {
  233. String fsc = settings.getString(Keys.web.forwardSlashCharacter, "/");
  234. String encodedPath = document.replace(' ', '-');
  235. try {
  236. encodedPath = URLEncoder.encode(encodedPath, "UTF-8");
  237. } catch (UnsupportedEncodingException e) {
  238. logger.error(null, e);
  239. }
  240. encodedPath = encodedPath.replace("/", fsc).replace("%2F", fsc);
  241. String url = RequestCycle.get().urlFor(pageClass, WicketUtils.newPathParameter(repositoryName, commitId, encodedPath)).toString();
  242. return url;
  243. }
  244. private String getDocumentName(final String document) {
  245. // extract document name
  246. String name = StringUtils.stripFileExtension(document);
  247. name = name.replace('_', ' ');
  248. if (name.indexOf('/') > -1) {
  249. name = name.substring(name.lastIndexOf('/') + 1);
  250. }
  251. return name;
  252. }
  253. public static class MarkupDocument implements Serializable {
  254. private static final long serialVersionUID = 1L;
  255. public final String documentPath;
  256. public final String markup;
  257. public final MarkupSyntax syntax;
  258. public String html;
  259. MarkupDocument(String documentPath, String markup, MarkupSyntax syntax) {
  260. this.documentPath = documentPath;
  261. this.markup = markup;
  262. this.syntax = syntax;
  263. }
  264. String getCurrentPath() {
  265. String basePath = "";
  266. if (documentPath.indexOf('/') > -1) {
  267. basePath = documentPath.substring(0, documentPath.lastIndexOf('/') + 1);
  268. if (basePath.charAt(0) == '/') {
  269. return basePath.substring(1);
  270. }
  271. }
  272. return basePath;
  273. }
  274. String getRelativePath(String ref) {
  275. return ref.charAt(0) == '/' ? ref.substring(1) : (getCurrentPath() + ref);
  276. }
  277. }
  278. }