| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324 |
- """
- Critic.
- pymdownx.critic
- Parses critic markup and outputs the file in a more visual HTML.
- Must be the last extension loaded.
- MIT license.
- Copyright (c) 2014 - 2017 Isaac Muse <isaacmuse@gmail.com>
- Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
- documentation files (the "Software"), to deal in the Software without restriction, including without limitation
- the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
- and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in all copies or substantial portions
- of the Software.
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
- TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
- THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
- CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
- DEALINGS IN THE SOFTWARE.
- """
- from markdown import Extension
- from markdown.preprocessors import Preprocessor
- from markdown.postprocessors import Postprocessor
- from markdown.util import STX, ETX
- import re
- SOH = '\u0001' # start
- EOT = '\u0004' # end
- CRITIC_KEY = "czjqqkd:%s"
- CRITIC_PLACEHOLDER = CRITIC_KEY % r'[0-9]+'
- SINGLE_CRITIC_PLACEHOLDER = r'%(stx)s(?P<key>%(key)s)%(etx)s' % {
- "key": CRITIC_PLACEHOLDER, "stx": STX, "etx": ETX
- }
- CRITIC_PLACEHOLDERS = r'''(?x)
- (?:
- (?P<block>\<p\>(?P<block_keys>(?:%(stx)s%(key)s%(etx)s)+)\</p\>) |
- %(single)s
- )
- ''' % {
- "key": CRITIC_PLACEHOLDER, "single": SINGLE_CRITIC_PLACEHOLDER,
- "stx": STX, "etx": ETX
- }
- ALL_CRITICS = r'''(?x)
- ((?P<critic>(?P<open>\{)
- (?:
- (?P<ins_open>\+{2})
- (?P<ins_text>.*?)
- (?P<ins_close>\+{2})
- | (?P<del_open>\-{2})
- (?P<del_text>.*?)
- (?P<del_close>\-{2})
- | (?P<mark_open>\={2})
- (?P<mark_text>.*?)
- (?P<mark_close>\={2})
- | (?P<comment>
- (?P<com_open>\>{2})
- (?P<com_text>.*?)
- (?P<com_close>\<{2})
- )
- | (?P<sub_open>\~{2})
- (?P<sub_del_text>.*?)
- (?P<sub_mid>\~\>)
- (?P<sub_ins_text>.*?)
- (?P<sub_close>\~{2})
- )
- (?P<close>\})))
- '''
- RE_CRITIC = re.compile(ALL_CRITICS, re.DOTALL)
- RE_CRITIC_PLACEHOLDER = re.compile(CRITIC_PLACEHOLDERS)
- RE_CRITIC_SUB_PLACEHOLDER = re.compile(SINGLE_CRITIC_PLACEHOLDER)
- RE_CRITIC_BLOCK = re.compile(r'((?:ins|del|mark)\s+)(class=([\'"]))(.*?)(\3)')
- RE_BLOCK_SEP = re.compile(r'^(?:\r?\n){2,}$')
- class CriticStash(object):
- """Stash critic marks until ready."""
- def __init__(self, stash_key):
- """Initialize."""
- self.stash_key = stash_key
- self.stash = {}
- self.count = 0
- def __len__(self): # pragma: no cover
- """Get length of stash."""
- return len(self.stash)
- def get(self, key, default=None):
- """Get the specified item from the stash."""
- code = self.stash.get(key, default)
- return code
- def remove(self, key): # pragma: no cover
- """Remove the specified item from the stash."""
- del self.stash[key]
- def store(self, code):
- """
- Store the code in the stash with the placeholder.
- Return placeholder.
- """
- key = self.stash_key % str(self.count)
- self.stash[key] = code
- self.count += 1
- return SOH + key + EOT
- def clear(self):
- """Clear the stash."""
- self.stash = {}
- self.count = 0
- class CriticsPostprocessor(Postprocessor):
- """Handle cleanup on post process for viewing critic marks."""
- def __init__(self, critic_stash):
- """Initialize."""
- super(CriticsPostprocessor, self).__init__()
- self.critic_stash = critic_stash
- def subrestore(self, m):
- """Replace all critic tags in the paragraph block `<p>(critic del close)(critic ins close)</p>` etc."""
- content = None
- key = m.group('key')
- if key is not None:
- content = self.critic_stash.get(key)
- return content
- def block_edit(self, m):
- """Handle block edits."""
- if 'break' in m.group(4).split(' '):
- return m.group(0)
- else:
- return m.group(1) + m.group(2) + m.group(4) + ' block' + m.group(5)
- def restore(self, m):
- """Replace placeholders with actual critic tags."""
- content = None
- if m.group('block_keys') is not None:
- content = RE_CRITIC_SUB_PLACEHOLDER.sub(
- self.subrestore, m.group('block_keys')
- )
- if content is not None:
- content = RE_CRITIC_BLOCK.sub(self.block_edit, content)
- else:
- text = self.critic_stash.get(m.group('key'))
- if text is not None:
- content = text
- return content if content is not None else m.group(0)
- def run(self, text):
- """Replace critic placeholders."""
- text = RE_CRITIC_PLACEHOLDER.sub(self.restore, text)
- return text
- class CriticViewPreprocessor(Preprocessor):
- """Handle viewing critic marks in Markdown content."""
- def __init__(self, critic_stash):
- """Initialize."""
- super(CriticViewPreprocessor, self).__init__()
- self.critic_stash = critic_stash
- def _ins(self, text):
- """Handle critic inserts."""
- if RE_BLOCK_SEP.match(text):
- return '\n\n%s\n\n' % self.critic_stash.store('<ins class="critic break"> </ins>')
- return (
- self.critic_stash.store('<ins class="critic">') +
- text +
- self.critic_stash.store('</ins>')
- )
- def _del(self, text):
- """Handle critic deletes."""
- if RE_BLOCK_SEP.match(text):
- return self.critic_stash.store('<del class="critic break"> </del>')
- return (
- self.critic_stash.store('<del class="critic">') +
- text +
- self.critic_stash.store('</del>')
- )
- def _mark(self, text):
- """Handle critic marks."""
- return (
- self.critic_stash.store('<mark class="critic">') +
- text +
- self.critic_stash.store('</mark>')
- )
- def _comment(self, text):
- """Handle critic comments."""
- return (
- self.critic_stash.store(
- '<span class="critic comment">' +
- self.html_escape(text, strip_nl=True) +
- '</span>'
- )
- )
- def critic_view(self, m):
- """Insert appropriate HTML to tags to visualize Critic marks."""
- if m.group('ins_open'):
- return self._ins(m.group('ins_text'))
- elif m.group('del_open'):
- return self._del(m.group('del_text'))
- elif m.group('sub_open'):
- return (
- self._del(m.group('sub_del_text')) +
- self._ins(m.group('sub_ins_text'))
- )
- elif m.group('mark_open'):
- return self._mark(m.group('mark_text'))
- elif m.group('com_open'):
- return self._comment(m.group('com_text'))
- def critic_parse(self, m):
- """
- Normal critic parser.
- Either removes accepted or rejected critic marks and replaces with the opposite.
- Comments are removed and marks are replaced with their content.
- """
- accept = self.config["mode"] == 'accept'
- if m.group('ins_open'):
- return m.group('ins_text') if accept else ''
- elif m.group('del_open'):
- return '' if accept else m.group('del_text')
- elif m.group('mark_open'):
- return m.group('mark_text')
- elif m.group('com_open'):
- return ''
- elif m.group('sub_open'):
- return m.group('sub_ins_text') if accept else m.group('sub_del_text')
- def html_escape(self, txt, strip_nl=False):
- """Basic html escaping."""
- txt = txt.replace('&', '&')
- txt = txt.replace('<', '<')
- txt = txt.replace('>', '>')
- txt = txt.replace('"', '"')
- txt = txt.replace("\n", "<br>" if not strip_nl else ' ')
- return txt
- def run(self, lines):
- """Process critic marks."""
- # Determine processor type to use
- if self.config['mode'] == "view":
- processor = self.critic_view
- else:
- processor = self.critic_parse
- # Find and process critic marks
- text = RE_CRITIC.sub(processor, '\n'.join(lines))
- return text.split('\n')
- class CriticExtension(Extension):
- """Critic extension."""
- def __init__(self, *args, **kwargs):
- """Initialize."""
- self.config = {
- 'mode': ['view', "Critic mode to run in ('view', 'accept', or 'reject') - Default: view "],
- 'raw_view': [False, "Raw view keeps the output as the raw markup for view mode - Default False"]
- }
- super(CriticExtension, self).__init__(*args, **kwargs)
- def extendMarkdown(self, md):
- """Register the extension."""
- md.registerExtension(self)
- self.critic_stash = CriticStash(CRITIC_KEY)
- post = CriticsPostprocessor(self.critic_stash)
- critic = CriticViewPreprocessor(self.critic_stash)
- critic.config = self.getConfigs()
- md.preprocessors.register(critic, "critic", 31.1)
- md.postprocessors.register(post, "critic-post", 25)
- md.registerExtensions(["pymdownx._bypassnorm"], {})
- def reset(self):
- """Clear stash."""
- self.critic_stash.clear()
- def makeExtension(*args, **kwargs):
- """Return extension."""
- return CriticExtension(*args, **kwargs)
|