diff --git a/zippydoc/__init__.py b/zippydoc/__init__.py index e2e7e54..d328b10 100644 --- a/zippydoc/__init__.py +++ b/zippydoc/__init__.py @@ -1,2 +1,4 @@ from block_markup import * -from parser import * +from document import * +from transformation_ruleset import * +from value import * diff --git a/zippydoc/block_markup.py b/zippydoc/block_markup.py index b7f07c2..3d1187d 100644 --- a/zippydoc/block_markup.py +++ b/zippydoc/block_markup.py @@ -1,4 +1,5 @@ import re +from value import Value class TreeLevel: def __init__(self, indentation, data): @@ -8,49 +9,17 @@ class TreeLevel: def add(self, element): self.elements.append(element) - - def output(self): - return self.render() - - def render_children(self): + + def transform(self, ruleset): + return self.transform_children(ruleset) + + def transform_children(self, ruleset): child_output = "" for child in self.elements: - child_output += child.output() + child_output += child.transform(ruleset) - return '
%s
' % child_output - - def process_inline_markup(self, text): - text = re.sub("`([^`]+)`", '\\1', text) # Fixed-width - text = re.sub("\*\*([^*]+)\*\*", "\\1", text) # Emphasized - text = re.sub("__([^_]+)__", "\\1", text) # Strong - text = re.sub("{>([^}]+)}\(([^)]+)\)", '\\2', text) # Hyperlink with text - text = re.sub("{>([^}]+)}", '\\1', text) # Hyperlink - text = re.sub("{([^}]+:[^}]+)}\(([^)]+)\)", '\\2', text) # External hyperlink with text - text = re.sub("{([^}]+:[^}]+)}", '\\1', text) # External hyperlink - text = re.sub("{<([^}]+)}\(([^)]+)\)", '\\2', text) # Forced external hyperlink with text - text = re.sub("{<([^}]+)}", '\\1', text) # Forced external hyperlink - - return text - - def clear_markup(self, text): - text = re.sub("`([^`]+)`", '\\1', text) # Fixed-width - text = re.sub("\*\*([^*]+)\*\*", "\\1", text) # Emphasized - text = re.sub("__([^_]+)__", "\\1", text) # Strong - text = re.sub("{>([^}]+)}\(([^)]+)\)", '\\2', text) # Hyperlink with text - text = re.sub("{>([^}]+)}", '\\1', text) # Hyperlink - text = re.sub("{([^}]+:[^}]+)}\(([^)]+)\)", '\\2', text) # External hyperlink with text - text = re.sub("{([^}]+:[^}]+)}", '\\1', text) # External hyperlink - text = re.sub("{<([^}]+)}\(([^)]+)\)", '\\2', text) # Forced external hyperlink with text - text = re.sub("{<([^}]+)}", '\\1', text) # Forced external hyperlink - - return text - - def fix_preformatted(self, text): - return text.replace("<", "<").replace(">", ">") - - def render(self): - return self.render_children() + return ruleset.transform_children(child_output) class Header(TreeLevel): def __init__(self, indentation, data, depth): @@ -59,83 +28,58 @@ class Header(TreeLevel): self.data = data self.depth = depth - def render(self): - if self.depth <= 7: - title_type = "h%d" % self.depth - else: - title_type = "h7" - - return "<%s>%s" % (title_type, self.data, title_type) + def transform(self, ruleset): + return ruleset.transform_header(self.depth, Value(self.data)) class Text(TreeLevel): - def render(self): - return '
%s
' % self.process_inline_markup(self.data) + def transform(self, ruleset): + return ruleset.transform_text(Value(self.data)) class Exclamation(TreeLevel): - def render(self): - return '
Important: %s
' % self.process_inline_markup(self.data) + def transform(self, ruleset): + return ruleset.transform_exclamation(Value(self.data), self.transform_children(ruleset)) class Definition(TreeLevel): - def get_anchor(self): - first = self.clear_markup(self.data.splitlines()[0]) - anchor = first.replace("...", "") - anchor = anchor.replace(".", "_") - anchor = re.sub("[^a-zA-Z0-9_]", "", anchor) - return anchor + def __init__(self, indentation, forms): + self.elements = [] + self.indentation = indentation + self.forms = [form.lstrip() for form in forms] + + def transform(self, ruleset): + return ruleset.transform_definition([Value(form) for form in self.forms], self.transform_children(ruleset)) + + def get_forms(self): + return [Value(form) for form in self.forms] def get_description(self): for element in self.elements: if element.__class__.__name__ == "Text": - data = self.process_inline_markup(element.data) - - if len(data) > 80: - matches = re.match("^(.{0,80})\W", data) - return matches.group(1) + "..." - else: - return data + return element.data return "" - def render(self): - return '
%s %s
' % (self.get_anchor(), self.process_inline_markup(self.data.replace("\n", "
")), self.render_children()) - class Argument(TreeLevel): def __init__(self, indentation, data, argname): self.elements = [] self.indentation = indentation self.data = data self.argname = argname - - def render(self): - return '
%s
%s%s
' % (self.argname, self.process_inline_markup(self.data), self.render_children()) + + def transform(self, ruleset): + return ruleset.transform_argument(Value(self.argname), Value(self.data), self.transform_children(ruleset)) class Example(TreeLevel): - def render(self): - return '
Example: %s %s
' % (self.data, self.render_children()) + def transform(self, ruleset): + return ruleset.transform_example(Value(self.data), self.transform_children(ruleset)) class Code(TreeLevel): - def render(self): - return 'Code:
%s
' % self.fix_preformatted(self.data) + def transform(self, ruleset): + return ruleset.transform_code(self.data) class Output(TreeLevel): - def render(self): - return 'Output:
%s
' % self.fix_preformatted(self.data) + def transform(self, ruleset): + return ruleset.transform_output(Value(self.data)) class Index(TreeLevel): - def render(self): - rendered = "" - - for item in self.data.toc_items: - forms = item.data.splitlines() - first = self.clear_markup(forms[0]) - - if len(forms) > 1: - rest = '(also: ' + ', '.join(self.clear_markup(form) for form in forms[1:]) + ")" - else: - rest = "" - - anchor = item.get_anchor() - description = item.get_description() - rendered += '
  • %s %s %s
  • ' % (anchor, first, description, rest) - - return '

    Table of contents

    ' % rendered + def transform(self, ruleset): + return ruleset.transform_toc([(definition, Value(definition.get_description())) for definition in self.data.get_definitions()]) diff --git a/zippydoc/document.py b/zippydoc/document.py new file mode 100644 index 0000000..bbfd741 --- /dev/null +++ b/zippydoc/document.py @@ -0,0 +1,85 @@ +import re +import block_markup + +class Document(): + def __init__(self, data): + self.data = data + self._parse() + + def _parse(self): + paragraphs = re.split("\s*\n\s*\n", self.data) + + self.paragraphs = paragraphs + self.definitions = [] + + current_level = 0 + current_paragraph = 0 + self.current_elements = {0: block_markup.TreeLevel(0, "root")} + + for paragraph in paragraphs: + if paragraph.strip() == "": + continue + + current_paragraph += 1 + indentation = len(paragraph) - len(paragraph.lstrip("\t")) + 1 + + if indentation > current_level + 1: + raise Exception("Invalid indentation found in paragraph %d" % current_paragraph) + + start = indentation - 1 + lines = [line[start:] for line in paragraph.splitlines()] + + if lines[0].startswith("#"): + # Header + depth = len(lines[0]) - len(lines[0].lstrip("#")) + lines[0] = lines[0].lstrip("# ") + element = block_markup.Header(indentation, " ".join(lines), depth) + elif lines[0].startswith("^"): + # Definition + lines[0] = lines[0].lstrip("^ ") + element = block_markup.Definition(indentation, lines) + self.definitions.append(element) + elif lines[0].startswith("@"): + # Example + lines[0] = lines[0].lstrip("@ ") + element = block_markup.Example(indentation, " ".join(lines)) + elif lines[0].startswith("$$") and self.current_elements[current_level].__class__.__name__ == "Code": + # Code continuation + self.current_elements[current_level].data += "\n\n" + "\n".join(lines).lstrip("$ ") + continue + elif lines[0].startswith("$"): + # Code block start + lines[0] = lines[0].lstrip("$ ") + element = block_markup.Code(indentation, "\n".join(lines)) + elif lines[0].startswith(">>") and self.current_elements[current_level].__class__.__name__ == "Output": + # Output continuation + self.current_elements[current_level].data += "\n\n" + "\n".join(lines).lstrip("> ") + continue + elif lines[0].startswith(">"): + # Output block start + lines[0] = lines[0].lstrip("> ") + element = block_markup.Output(indentation, "\n".join(lines)) + elif lines[0].startswith("!"): + # Exclamation + lines[0] = lines[0].lstrip("! ") + element = block_markup.Exclamation(indentation, " ".join(lines)) + elif re.match(".*::\s*$", lines[0]): + # Argument definition + argname = re.match("(.*)::\s*$", lines[0]).group(1) + element = block_markup.Argument(indentation, " ".join(line.lstrip() for line in lines[1:]), argname) + elif lines[0].strip() == "{TOC}": + # Table of contents + element = block_markup.Index(indentation, self) + else: + # Text + element = block_markup.Text(indentation, " ".join(lines)) + + self.current_elements[indentation - 1].add(element) + current_level = indentation + self.current_elements[current_level] = element + + def transform(self, ruleset): + return self.current_elements[0].transform(ruleset) + + def get_definitions(self): + return self.definitions diff --git a/zippydoc/parser.py b/zippydoc/parser.py deleted file mode 100644 index 57cdbc0..0000000 --- a/zippydoc/parser.py +++ /dev/null @@ -1,86 +0,0 @@ -from block_markup import * - -class Parser(): - def __init__(self, template): - self.template = template - - def render(self, text): - paragraphs = re.split("\s*\n\s*\n", text) - self.toc_items = [] - current_level = 0 - current_paragraph = 0 - current_elements = {0: TreeLevel(0, "root")} - - for paragraph in paragraphs: - if paragraph.strip() == "": - continue - - current_paragraph += 1 - indentation = len(paragraph) - len(paragraph.lstrip("\t")) + 1 - - if indentation > current_level + 1: - raise Exception("Invalid indentation found in paragraph %d" % current_paragraph) - - element_type = TreeLevel - start = indentation - 1 - - lines = [line[start:] for line in paragraph.splitlines()] - - if lines[0].startswith("#"): - element_type = Header - depth = len(lines[0]) - len(lines[0].lstrip("#")) - lines[0] = lines[0].lstrip("# ") - data = " ".join(lines) - elif lines[0].startswith("^"): - element_type = Definition - lines[0] = lines[0].lstrip("^ ") - data = "\n".join(lines) - elif lines[0].startswith("@"): - element_type = Example - lines[0] = lines[0].lstrip("@ ") - data = " ".join(lines) - elif lines[0].startswith("$$") and current_elements[current_level].__class__.__name__ == "Code": - current_elements[current_level].data += "\n\n" + "\n".join(lines).lstrip("$ ") - continue - elif lines[0].startswith("$"): - element_type = Code - lines[0] = lines[0].lstrip("$ ") - data = "\n".join(lines) - elif lines[0].startswith(">>") and current_elements[current_level].__class__.__name__ == "Output": - current_elements[current_level].data += "\n\n" + "\n".join(lines).lstrip("> ") - continue - elif lines[0].startswith(">"): - element_type = Output - lines[0] = lines[0].lstrip("> ") - data = "\n".join(lines) - elif lines[0].startswith("!"): - element_type = Exclamation - lines[0] = lines[0].lstrip("! ") - data = " ".join(lines) - elif re.match(".*::\s*$", lines[0]): - element_type = Argument - argname = lines[0][:-2] - data = " ".join(line.lstrip() for line in lines[1:]) - elif lines[0].strip() == "{TOC}": - element_type = Index - data = self - else: - element_type = Text - data = " ".join(lines) - - if element_type.__name__ == "Header": - element = Header(indentation, data, depth) - elif element_type.__name__ == "Argument": - element = Argument(indentation, data, argname) - else: - element = element_type(indentation, data) - - if element_type.__name__ == "Definition": - self.toc_items.append(element) - - current_elements[indentation - 1].add(element) - - current_level = indentation - current_elements[current_level] = element - - return self.template.replace("{CONTENT}", current_elements[0].output()) diff --git a/zippydoc/transformation_ruleset.py b/zippydoc/transformation_ruleset.py new file mode 100644 index 0000000..d1b1c37 --- /dev/null +++ b/zippydoc/transformation_ruleset.py @@ -0,0 +1,45 @@ +class TransformationRuleset(): + def transform_children(self, text): + pass + + def transform_header(self, depth, text): + pass + + def transform_definition(self, forms, children): + pass + + def transform_argument(self, name, description, children): + pass + + def transform_example(self, title, children): + pass + + def transform_code(self, text): + pass + + def transform_output(self, text): + pass + + def transform_exclamation(self, text, children): + pass + + def transform_text(self, text): + pass + + def transform_reference(self, target, description): + pass + + def transform_external_reference(self, target, description): + pass + + def transform_fixed_width(self, text): + pass + + def transform_emphasis(self, text): + pass + + def transform_strong(self, text): + pass + + def transform_toc(self, items): + pass diff --git a/zippydoc/value.py b/zippydoc/value.py new file mode 100644 index 0000000..f63cd27 --- /dev/null +++ b/zippydoc/value.py @@ -0,0 +1,29 @@ +import re + +class Value(str): + def transform(self, ruleset): + text = self + text = re.sub("`([^`]+)`", lambda x: ruleset.transform_fixed_width(Value(x.group(1))), text) # Fixed-width + text = re.sub("\*\*([^*]+)\*\*", lambda x: ruleset.transform_emphasis(Value(x.group(1))), text) # Emphasized + text = re.sub("__([^_]+)__", lambda x: ruleset.transform_strong(Value(x.group(1))), text) # Strong + text = re.sub("{>([^}]+)}\(([^)]+)\)", lambda x: ruleset.transform_reference(Value(x.group(1)), Value(x.group(2))), text) # Hyperlink with text + text = re.sub("{>([^}]+)}", lambda x: ruleset.transform_reference(Value(x.group(1)), Value(x.group(1))), text) # Hyperlink + text = re.sub("{([^}]+:[^}]+)}\(([^)]+)\)", lambda x: ruleset.transform_external_reference(Value(x.group(1)), Value(x.group(2))), text) # External hyperlink with text + text = re.sub("{([^}]+:[^}]+)}", lambda x: ruleset.transform_external_reference(Value(x.group(1)), Value(x.group(1))), text) # External hyperlink + text = re.sub("{<([^}]+)}\(([^)]+)\)", lambda x: ruleset.transform_external_reference(Value(x.group(1)), Value(x.group(2))), text) # Forced external hyperlink with text + text = re.sub("{<([^}]+)}", lambda x: ruleset.transform_external_reference(Value(x.group(1)), Value(x.group(1))), text) # Forced external hyperlink + return text + + def clean(self): + text = self + text = re.sub("`([^`]+)`", '\\1', text) # Fixed-width + text = re.sub("\*\*([^*]+)\*\*", "\\1", text) # Emphasized + text = re.sub("__([^_]+)__", "\\1", text) # Strong + text = re.sub("{>([^}]+)}\(([^)]+)\)", '\\2', text) # Hyperlink with text + text = re.sub("{>([^}]+)}", '\\1', text) # Hyperlink + text = re.sub("{([^}]+:[^}]+)}\(([^)]+)\)", '\\2', text) # External hyperlink with text + text = re.sub("{([^}]+:[^}]+)}", '\\1', text) # External hyperlink + text = re.sub("{<([^}]+)}\(([^)]+)\)", '\\2', text) # Forced external hyperlink with text + text = re.sub("{<([^}]+)}", '\\1', text) # Forced external hyperlink + return text + diff --git a/zpy2html.py b/zpy2html.py index f391d23..7330883 100644 --- a/zpy2html.py +++ b/zpy2html.py @@ -9,9 +9,91 @@ parser.add_argument('files', metavar='FILE', type=str, nargs='+', args = parser.parse_args() options = vars(args) +class HtmlRuleset(zippydoc.TransformationRuleset): + def create_anchor(self, title): + anchor = title.clean().replace("...", "").replace(".", "_") + anchor = re.sub("[^a-zA-Z0-9_]", "", anchor) + return anchor + + def escape_html(self, text): + return text.replace("<", "<").replace(">", ">") + + def transform_children(self, text): + return '
    %s
    ' % text + + def transform_header(self, depth, text): + if depth <= 7: + title_type = "h%d" % depth + else: + title_type = "h7" + + return "<%s>%s" % (title_type, text.transform(self), title_type) + + def transform_definition(self, forms, children): + anchor = self.create_anchor(forms[0]) + formlist = "
    ".join([form.transform(self) for form in forms]) + return '
    %s %s
    ' % (anchor, formlist, children) + + def transform_argument(self, name, description, children): + return "
    %s
    %s%s
    " % (name, description.transform(self), children) + + def transform_example(self, title, children): + return '
    Example: %s %s
    ' % (title.transform(self), children) + + def transform_code(self, text): + return 'Code:
    %s
    ' % self.escape_html(text) + + def transform_output(self, text): + return 'Output:
    %s
    ' % self.escape_html(text) + + def transform_exclamation(self, text, children): + return '
    Important: %s %s
    ' % (text.transform(self), children) + + def transform_text(self, text): + return '
    %s
    ' % text.transform(self) + + def transform_reference(self, target, description): + return '%s' % (target, description.transform(self)) + + def transform_external_reference(self, target, description): + return '%s' % (target, description.transform(self)) + + def transform_fixed_width(self, text): + return '%s' % text + + def transform_emphasis(self, text): + return "%s" % text.transform(self) + + def transform_strong(self, text): + return "%s" % text.transform(self) + + def transform_toc(self, items): + rendered = "" + + for item in items: + forms = item[0].get_forms() + anchor = self.create_anchor(forms[0]) + + if len(forms) > 1: + alternatives = '(also: %s)' % ", ".join(form.clean() for form in forms[1:]) + else: + alternatives = "" + + description = item[1] + + if len(description) > 80: + matches = re.match("^(.{0,80})\W", data) + description = matches.group(1) + "..." + + description = zippydoc.Value(description).transform(self) + + rendered += '
  • %s %s %s
  • ' % (anchor, forms[0].clean(), description, alternatives) + + return '

    Table of contents

    ' % rendered + files = options["files"] -docparser = zippydoc.Parser(open("template.html").read()) +template = open("template.html").read() for zpy in files: destination = os.path.splitext(zpy)[0] + ".html" @@ -20,10 +102,12 @@ for zpy in files: data = f.read() f.close() - rendered = docparser.render(data) + doc = zippydoc.Document(open(zpy, "r").read()) + + rendered = doc.transform(HtmlRuleset()) f = open(destination, "w") - f.write(rendered) + f.write(template.replace("{CONTENT}", rendered)) f.close() print "Rendered %s" % destination