| 1 | # -*- coding: utf-8 -*- |
|---|
| 2 | # |
|---|
| 3 | # Copyright (C) 2016 tkob <ether4@gmail.com> |
|---|
| 4 | # All rights reserved. |
|---|
| 5 | # |
|---|
| 6 | # This software is licensed as described in the file COPYING, which |
|---|
| 7 | # you should have received as part of this distribution. |
|---|
| 8 | |
|---|
| 9 | import re |
|---|
| 10 | import uuid |
|---|
| 11 | |
|---|
| 12 | from trac.core import implements |
|---|
| 13 | from trac.util.text import unicode_quote |
|---|
| 14 | from trac.util.html import Fragment, Element, escape |
|---|
| 15 | from trac.web.chrome import (add_script, add_script_data, add_stylesheet, |
|---|
| 16 | Chrome, ITemplateProvider) |
|---|
| 17 | from trac.web.main import IRequestHandler |
|---|
| 18 | from trac.wiki.api import IWikiPageManipulator |
|---|
| 19 | from trac.wiki.formatter import extract_link |
|---|
| 20 | from trac.wiki.macros import WikiMacroBase |
|---|
| 21 | from trac.wiki.model import WikiPage |
|---|
| 22 | |
|---|
| 23 | |
|---|
| 24 | class MermaidMacro(WikiMacroBase): |
|---|
| 25 | implements(ITemplateProvider, IRequestHandler, IWikiPageManipulator) |
|---|
| 26 | |
|---|
| 27 | def expand_macro(self, formatter, name, content, args=None): |
|---|
| 28 | self.log.debug("content %s", content) |
|---|
| 29 | context = formatter.context |
|---|
| 30 | req = formatter.req |
|---|
| 31 | if args is None or 'id' not in args: |
|---|
| 32 | id_attr = '' |
|---|
| 33 | else: |
|---|
| 34 | id_attr = 'id="%s"' % escape(args['id']) |
|---|
| 35 | if not req: |
|---|
| 36 | # Off-line rendering (there's a command line API for mermaid) |
|---|
| 37 | return '<img alt="not-yet-implemented"/>' |
|---|
| 38 | Chrome(self.env).add_jquery_ui(req) |
|---|
| 39 | add_stylesheet(req, 'mermaid/mermaid.css') |
|---|
| 40 | add_script(req, 'mermaid/mermaid.min.js') |
|---|
| 41 | add_script(req, 'mermaid/tracmermaid.js') |
|---|
| 42 | add_script_data(req, { |
|---|
| 43 | '_tracmermaid': { |
|---|
| 44 | 'submit': req.href + '/mermaid/submit', |
|---|
| 45 | }, |
|---|
| 46 | 'form_token': req.form_token, |
|---|
| 47 | }) |
|---|
| 48 | content = self.expand_links(context, content) |
|---|
| 49 | return """\ |
|---|
| 50 | <div class="mermaid" |
|---|
| 51 | %s |
|---|
| 52 | data-mermaidresourcerealm="%s" |
|---|
| 53 | data-mermaidresourceid="%s" |
|---|
| 54 | data-mermaidresourceversion="%s" |
|---|
| 55 | data-mermaidsource="%s">%s |
|---|
| 56 | </div> |
|---|
| 57 | <script type="text/javascript"> |
|---|
| 58 | if (typeof mermaid !== 'undefined') { |
|---|
| 59 | mermaid.init(); // ok to call repeatedly (data-processed) |
|---|
| 60 | $(".mermaid g[title]").css('cursor', 'pointer'); |
|---|
| 61 | } |
|---|
| 62 | </script>""" % ( |
|---|
| 63 | id_attr, |
|---|
| 64 | escape(context.resource.realm), |
|---|
| 65 | escape(unicode(context.resource.id)), |
|---|
| 66 | escape(unicode(context.resource.version or '')), |
|---|
| 67 | escape(unicode_quote(content)), |
|---|
| 68 | escape(content)) |
|---|
| 69 | |
|---|
| 70 | click_re = re.compile(r'^\s*click\s+\w+\s+(trac)\s+(.*)$') |
|---|
| 71 | |
|---|
| 72 | def expand_links(self, context, content): |
|---|
| 73 | lines = [] |
|---|
| 74 | for line in content.splitlines(): |
|---|
| 75 | # "Native" mermaid link (left alone): |
|---|
| 76 | # click A callback "This is a tooltip for a link" |
|---|
| 77 | # click B "http://www.github.com" "This is a tooltip for a link" |
|---|
| 78 | # |
|---|
| 79 | # TracLinks link (transformed to native): |
|---|
| 80 | # click A trac TracLinks |
|---|
| 81 | # click A trac r123 |
|---|
| 82 | # click A trac [123] |
|---|
| 83 | # click A trac [[TracLinks|Anything that can be parsed as a link]] |
|---|
| 84 | m = self.click_re.match(line) |
|---|
| 85 | if m: |
|---|
| 86 | link = m.group(2) |
|---|
| 87 | link = extract_link(self.env, context, link) |
|---|
| 88 | if isinstance(link, Element): |
|---|
| 89 | href = link.attrib.get('href') |
|---|
| 90 | title = link.attrib.get('title', '') |
|---|
| 91 | for c in link.children: |
|---|
| 92 | if not isinstance(c, Fragment): |
|---|
| 93 | name = c |
|---|
| 94 | break |
|---|
| 95 | else: |
|---|
| 96 | name = title |
|---|
| 97 | line = line[0:m.start(1)] + '"%s" "%s"' % ( |
|---|
| 98 | href.replace('"', ''), name.replace('"', '')) |
|---|
| 99 | lines.append(line) |
|---|
| 100 | return '\n'.join(lines) |
|---|
| 101 | |
|---|
| 102 | # ITemplateProvider methods |
|---|
| 103 | |
|---|
| 104 | def get_htdocs_dirs(self): |
|---|
| 105 | from pkg_resources import resource_filename |
|---|
| 106 | return [('mermaid', resource_filename(__name__, 'htdocs'))] |
|---|
| 107 | |
|---|
| 108 | def get_templates_dirs(self): |
|---|
| 109 | return [] |
|---|
| 110 | |
|---|
| 111 | # IRequestHandler methods |
|---|
| 112 | |
|---|
| 113 | def match_request(self, req): |
|---|
| 114 | self.log.debug('match_request: path_info=' + req.path_info) |
|---|
| 115 | return req.path_info.startswith('/mermaid/submit') |
|---|
| 116 | |
|---|
| 117 | def process_request(self, req): |
|---|
| 118 | self.log.debug('process_request: req=' + str(req)) |
|---|
| 119 | self.log.debug('process_request: args=' + str(req.args)) |
|---|
| 120 | |
|---|
| 121 | id = req.args['id'] |
|---|
| 122 | page_name = req.args['wikipage'] |
|---|
| 123 | source = req.args['source'] |
|---|
| 124 | page = WikiPage(env=self.env, name=page_name) |
|---|
| 125 | lines = page.text.splitlines() |
|---|
| 126 | lines.reverse() |
|---|
| 127 | buf = [] |
|---|
| 128 | while len(lines) > 0: |
|---|
| 129 | line = lines.pop() |
|---|
| 130 | if line == ('{{{#!Mermaid id="%s"' % id): |
|---|
| 131 | buf.append(line) |
|---|
| 132 | buf.append(source) |
|---|
| 133 | while len(lines) > 0: |
|---|
| 134 | line = lines.pop() |
|---|
| 135 | if line.rstrip() == "}}}": |
|---|
| 136 | buf.append(line) |
|---|
| 137 | break |
|---|
| 138 | else: |
|---|
| 139 | buf.append(line) |
|---|
| 140 | page.text = "\n".join(buf) |
|---|
| 141 | |
|---|
| 142 | req.perm(page).require('WIKI_MODIFY') |
|---|
| 143 | comment = "Update from Mermaid live editor" |
|---|
| 144 | page.save(req.authname, comment, req.remote_addr) |
|---|
| 145 | |
|---|
| 146 | req.send("OK", 'text/plain') |
|---|
| 147 | |
|---|
| 148 | # IWikiPageManipulator methods |
|---|
| 149 | |
|---|
| 150 | def prepare_wiki_page(self, req, page, fields): |
|---|
| 151 | pass |
|---|
| 152 | |
|---|
| 153 | def validate_wiki_page(self, req, page): |
|---|
| 154 | buf = [] |
|---|
| 155 | for line in page.text.splitlines(): |
|---|
| 156 | if line == "{{{#!Mermaid": |
|---|
| 157 | buf.append('{{{#!Mermaid id="%s"' % str(uuid.uuid1())) |
|---|
| 158 | else: |
|---|
| 159 | buf.append(line) |
|---|
| 160 | page.text = "\n".join(buf) |
|---|
| 161 | return [] |
|---|