You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

MarkupProcessor.java 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396
  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 static org.pegdown.FastEncoder.encode;
  18. import java.io.Serializable;
  19. import java.io.StringWriter;
  20. import java.io.UnsupportedEncodingException;
  21. import java.net.URLEncoder;
  22. import java.text.MessageFormat;
  23. import java.util.ArrayList;
  24. import java.util.Arrays;
  25. import java.util.HashMap;
  26. import java.util.List;
  27. import java.util.Map;
  28. import org.apache.wicket.Page;
  29. import org.apache.wicket.RequestCycle;
  30. import org.eclipse.jgit.lib.Repository;
  31. import org.eclipse.jgit.revwalk.RevCommit;
  32. import org.eclipse.mylyn.wikitext.confluence.core.ConfluenceLanguage;
  33. import org.eclipse.mylyn.wikitext.core.parser.Attributes;
  34. import org.eclipse.mylyn.wikitext.core.parser.MarkupParser;
  35. import org.eclipse.mylyn.wikitext.core.parser.builder.HtmlDocumentBuilder;
  36. import org.eclipse.mylyn.wikitext.core.parser.markup.MarkupLanguage;
  37. import org.eclipse.mylyn.wikitext.mediawiki.core.MediaWikiLanguage;
  38. import org.eclipse.mylyn.wikitext.textile.core.TextileLanguage;
  39. import org.eclipse.mylyn.wikitext.tracwiki.core.TracWikiLanguage;
  40. import org.eclipse.mylyn.wikitext.twiki.core.TWikiLanguage;
  41. import org.pegdown.LinkRenderer;
  42. import org.pegdown.ast.ExpImageNode;
  43. import org.pegdown.ast.RefImageNode;
  44. import org.pegdown.ast.WikiLinkNode;
  45. import org.slf4j.Logger;
  46. import org.slf4j.LoggerFactory;
  47. import com.gitblit.IStoredSettings;
  48. import com.gitblit.Keys;
  49. import com.gitblit.models.PathModel;
  50. import com.gitblit.utils.JGitUtils;
  51. import com.gitblit.utils.MarkdownUtils;
  52. import com.gitblit.utils.StringUtils;
  53. import com.gitblit.wicket.pages.DocPage;
  54. import com.gitblit.wicket.pages.RawPage;
  55. /**
  56. * Processes markup content and generates html with repository-relative page and
  57. * image linking.
  58. *
  59. * @author James Moger
  60. *
  61. */
  62. public class MarkupProcessor {
  63. public enum MarkupSyntax {
  64. PLAIN, MARKDOWN, TWIKI, TRACWIKI, TEXTILE, MEDIAWIKI, CONFLUENCE
  65. }
  66. private Logger logger = LoggerFactory.getLogger(getClass());
  67. private final IStoredSettings settings;
  68. public MarkupProcessor(IStoredSettings settings) {
  69. this.settings = settings;
  70. }
  71. public List<String> getMarkupExtensions() {
  72. List<String> list = new ArrayList<String>();
  73. list.addAll(settings.getStrings(Keys.web.confluenceExtensions));
  74. list.addAll(settings.getStrings(Keys.web.markdownExtensions));
  75. list.addAll(settings.getStrings(Keys.web.mediawikiExtensions));
  76. list.addAll(settings.getStrings(Keys.web.textileExtensions));
  77. list.addAll(settings.getStrings(Keys.web.tracwikiExtensions));
  78. list.addAll(settings.getStrings(Keys.web.twikiExtensions));
  79. return list;
  80. }
  81. public List<String> getAllExtensions() {
  82. List<String> list = getMarkupExtensions();
  83. list.add("txt");
  84. list.add("TXT");
  85. return list;
  86. }
  87. private List<String> getRoots() {
  88. return settings.getStrings(Keys.web.documents);
  89. }
  90. private String [] getEncodings() {
  91. return settings.getStrings(Keys.web.blobEncodings).toArray(new String[0]);
  92. }
  93. private MarkupSyntax determineSyntax(String documentPath) {
  94. String ext = StringUtils.getFileExtension(documentPath).toLowerCase();
  95. if (StringUtils.isEmpty(ext)) {
  96. return MarkupSyntax.PLAIN;
  97. }
  98. if (settings.getStrings(Keys.web.confluenceExtensions).contains(ext)) {
  99. return MarkupSyntax.CONFLUENCE;
  100. } else if (settings.getStrings(Keys.web.markdownExtensions).contains(ext)) {
  101. return MarkupSyntax.MARKDOWN;
  102. } else if (settings.getStrings(Keys.web.mediawikiExtensions).contains(ext)) {
  103. return MarkupSyntax.MEDIAWIKI;
  104. } else if (settings.getStrings(Keys.web.textileExtensions).contains(ext)) {
  105. return MarkupSyntax.TEXTILE;
  106. } else if (settings.getStrings(Keys.web.tracwikiExtensions).contains(ext)) {
  107. return MarkupSyntax.TRACWIKI;
  108. } else if (settings.getStrings(Keys.web.twikiExtensions).contains(ext)) {
  109. return MarkupSyntax.TWIKI;
  110. }
  111. return MarkupSyntax.PLAIN;
  112. }
  113. public boolean hasRootDocs(Repository r) {
  114. List<String> roots = getRoots();
  115. List<String> extensions = getAllExtensions();
  116. List<PathModel> paths = JGitUtils.getFilesInPath(r, null, null);
  117. for (PathModel path : paths) {
  118. if (!path.isTree()) {
  119. String ext = StringUtils.getFileExtension(path.name).toLowerCase();
  120. String name = StringUtils.stripFileExtension(path.name).toLowerCase();
  121. if (roots.contains(name)) {
  122. if (StringUtils.isEmpty(ext) || extensions.contains(ext)) {
  123. return true;
  124. }
  125. }
  126. }
  127. }
  128. return false;
  129. }
  130. public List<MarkupDocument> getRootDocs(Repository r, String repositoryName, String commitId) {
  131. List<String> roots = getRoots();
  132. List<MarkupDocument> list = getDocs(r, repositoryName, commitId, roots);
  133. return list;
  134. }
  135. public MarkupDocument getReadme(Repository r, String repositoryName, String commitId) {
  136. List<MarkupDocument> list = getDocs(r, repositoryName, commitId, Arrays.asList("readme"));
  137. if (list.isEmpty()) {
  138. return null;
  139. }
  140. return list.get(0);
  141. }
  142. private List<MarkupDocument> getDocs(Repository r, String repositoryName, String commitId, List<String> names) {
  143. List<String> extensions = getAllExtensions();
  144. String [] encodings = getEncodings();
  145. Map<String, MarkupDocument> map = new HashMap<String, MarkupDocument>();
  146. RevCommit commit = JGitUtils.getCommit(r, commitId);
  147. List<PathModel> paths = JGitUtils.getFilesInPath(r, null, commit);
  148. for (PathModel path : paths) {
  149. if (!path.isTree()) {
  150. String ext = StringUtils.getFileExtension(path.name).toLowerCase();
  151. String name = StringUtils.stripFileExtension(path.name).toLowerCase();
  152. if (names.contains(name)) {
  153. if (StringUtils.isEmpty(ext) || extensions.contains(ext)) {
  154. String markup = JGitUtils.getStringContent(r, commit.getTree(), path.name, encodings);
  155. MarkupDocument doc = parse(repositoryName, commitId, path.name, markup);
  156. map.put(name, doc);
  157. }
  158. }
  159. }
  160. }
  161. // return document list in requested order
  162. List<MarkupDocument> list = new ArrayList<MarkupDocument>();
  163. for (String name : names) {
  164. if (map.containsKey(name)) {
  165. list.add(map.get(name));
  166. }
  167. }
  168. return list;
  169. }
  170. public MarkupDocument parse(String repositoryName, String commitId, String documentPath, String markupText) {
  171. final MarkupSyntax syntax = determineSyntax(documentPath);
  172. final MarkupDocument doc = new MarkupDocument(documentPath, markupText, syntax);
  173. if (markupText != null) {
  174. try {
  175. switch (syntax){
  176. case CONFLUENCE:
  177. parse(doc, repositoryName, commitId, new ConfluenceLanguage());
  178. break;
  179. case MARKDOWN:
  180. parse(doc, repositoryName, commitId);
  181. break;
  182. case MEDIAWIKI:
  183. parse(doc, repositoryName, commitId, new MediaWikiLanguage());
  184. break;
  185. case TEXTILE:
  186. parse(doc, repositoryName, commitId, new TextileLanguage());
  187. break;
  188. case TRACWIKI:
  189. parse(doc, repositoryName, commitId, new TracWikiLanguage());
  190. break;
  191. case TWIKI:
  192. parse(doc, repositoryName, commitId, new TWikiLanguage());
  193. break;
  194. default:
  195. doc.html = MarkdownUtils.transformPlainText(markupText);
  196. break;
  197. }
  198. } catch (Exception e) {
  199. logger.error("failed to transform " + syntax, e);
  200. }
  201. }
  202. if (doc.html == null) {
  203. // failed to transform markup
  204. if (markupText == null) {
  205. markupText = String.format("Document <b>%1$s</b> not found in <em>%2$s</em>", documentPath, repositoryName);
  206. }
  207. markupText = MessageFormat.format("<div class=\"alert alert-error\"><strong>{0}:</strong> {1}</div>{2}", "Error", "failed to parse markup", markupText);
  208. doc.html = StringUtils.breakLinesForHtml(markupText);
  209. }
  210. return doc;
  211. }
  212. /**
  213. * Parses the markup using the specified markup language
  214. *
  215. * @param doc
  216. * @param repositoryName
  217. * @param commitId
  218. * @param lang
  219. */
  220. private void parse(final MarkupDocument doc, final String repositoryName, final String commitId, MarkupLanguage lang) {
  221. StringWriter writer = new StringWriter();
  222. HtmlDocumentBuilder builder = new HtmlDocumentBuilder(writer) {
  223. @Override
  224. public void image(Attributes attributes, String imagePath) {
  225. String url;
  226. if (imagePath.indexOf("://") == -1) {
  227. // relative image
  228. String path = doc.getRelativePath(imagePath);
  229. url = getWicketUrl(RawPage.class, repositoryName, commitId, path);
  230. } else {
  231. // absolute image
  232. url = imagePath;
  233. }
  234. super.image(attributes, url);
  235. }
  236. @Override
  237. public void link(Attributes attributes, String hrefOrHashName, String text) {
  238. String url;
  239. if (hrefOrHashName.charAt(0) != '#') {
  240. if (hrefOrHashName.indexOf("://") == -1) {
  241. // relative link
  242. String path = doc.getRelativePath(hrefOrHashName);
  243. url = getWicketUrl(DocPage.class, repositoryName, commitId, path);
  244. } else {
  245. // absolute link
  246. url = hrefOrHashName;
  247. }
  248. } else {
  249. // page-relative hash link
  250. url = hrefOrHashName;
  251. }
  252. super.link(attributes, url, text);
  253. }
  254. };
  255. // avoid the <html> and <body> tags
  256. builder.setEmitAsDocument(false);
  257. MarkupParser parser = new MarkupParser(lang);
  258. parser.setBuilder(builder);
  259. parser.parse(doc.markup);
  260. doc.html = writer.toString();
  261. }
  262. /**
  263. * Parses the document as Markdown using Pegdown.
  264. *
  265. * @param doc
  266. * @param repositoryName
  267. * @param commitId
  268. */
  269. private void parse(final MarkupDocument doc, final String repositoryName, final String commitId) {
  270. LinkRenderer renderer = new LinkRenderer() {
  271. @Override
  272. public Rendering render(ExpImageNode node, String text) {
  273. if (node.url.indexOf("://") == -1) {
  274. // repository-relative image link
  275. String path = doc.getRelativePath(node.url);
  276. String url = getWicketUrl(RawPage.class, repositoryName, commitId, path);
  277. return new Rendering(url, text);
  278. }
  279. // absolute image link
  280. return new Rendering(node.url, text);
  281. }
  282. @Override
  283. public Rendering render(RefImageNode node, String url, String title, String alt) {
  284. Rendering rendering;
  285. if (url.indexOf("://") == -1) {
  286. // repository-relative image link
  287. String path = doc.getRelativePath(url);
  288. String wurl = getWicketUrl(RawPage.class, repositoryName, commitId, path);
  289. rendering = new Rendering(wurl, alt);
  290. } else {
  291. // absolute image link
  292. rendering = new Rendering(url, alt);
  293. }
  294. return StringUtils.isEmpty(title) ? rendering : rendering.withAttribute("title", encode(title));
  295. }
  296. @Override
  297. public Rendering render(WikiLinkNode node) {
  298. String path = doc.getRelativePath(node.getText());
  299. String name = getDocumentName(path);
  300. String url = getWicketUrl(DocPage.class, repositoryName, commitId, path);
  301. return new Rendering(url, name);
  302. }
  303. };
  304. doc.html = MarkdownUtils.transformMarkdown(doc.markup, renderer);
  305. }
  306. private String getWicketUrl(Class<? extends Page> pageClass, final String repositoryName, final String commitId, final String document) {
  307. String fsc = settings.getString(Keys.web.forwardSlashCharacter, "/");
  308. String encodedPath = document.replace(' ', '-');
  309. try {
  310. encodedPath = URLEncoder.encode(encodedPath, "UTF-8");
  311. } catch (UnsupportedEncodingException e) {
  312. logger.error(null, e);
  313. }
  314. encodedPath = encodedPath.replace("/", fsc).replace("%2F", fsc);
  315. String url = RequestCycle.get().urlFor(pageClass, WicketUtils.newPathParameter(repositoryName, commitId, encodedPath)).toString();
  316. return url;
  317. }
  318. private String getDocumentName(final String document) {
  319. // extract document name
  320. String name = StringUtils.stripFileExtension(document);
  321. name = name.replace('_', ' ');
  322. if (name.indexOf('/') > -1) {
  323. name = name.substring(name.lastIndexOf('/') + 1);
  324. }
  325. return name;
  326. }
  327. public static class MarkupDocument implements Serializable {
  328. private static final long serialVersionUID = 1L;
  329. public final String documentPath;
  330. public final String markup;
  331. public final MarkupSyntax syntax;
  332. public String html;
  333. MarkupDocument(String documentPath, String markup, MarkupSyntax syntax) {
  334. this.documentPath = documentPath;
  335. this.markup = markup;
  336. this.syntax = syntax;
  337. }
  338. String getCurrentPath() {
  339. String basePath = "";
  340. if (documentPath.indexOf('/') > -1) {
  341. basePath = documentPath.substring(0, documentPath.lastIndexOf('/') + 1);
  342. if (basePath.charAt(0) == '/') {
  343. return basePath.substring(1);
  344. }
  345. }
  346. return basePath;
  347. }
  348. String getRelativePath(String ref) {
  349. return ref.charAt(0) == '/' ? ref.substring(1) : (getCurrentPath() + ref);
  350. }
  351. }
  352. }