kdesc --katoms is Klammertext rendered from ref/, the types in titled groups, with --katoms full for the regex column. @table :leading L is L lines of the surrounding text in html as in pdf (default 1), and :rulesep spaces the horizontal rules. The html table has one style attribute, so :column_width fractions take effect again. @code and @source_listing render in plain text; captions are numbered per kind there, and a table needs no blank line around it. @c takes :nd, and an option-shaped word before a bar inside literal code is code. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
503 lines
24 KiB
Python
503 lines
24 KiB
Python
# if __name__ == "__main__":
|
||
# import sys
|
||
# sys.path.append("../kutil")
|
||
# sys.path.append("../target")
|
||
|
||
import re
|
||
import klammer_base
|
||
import kutil
|
||
import html_util
|
||
import txt_util
|
||
import phases
|
||
from html_util import E
|
||
import latex_util as L
|
||
|
||
def escape_newlines(s):
|
||
# A newline becomes the marker the html paragraph pass turns back into a
|
||
# line break, so a verbatim source file keeps its lines. Used by Source.
|
||
return re.sub("\n", " ___NL___ ", s)
|
||
|
||
def is_comment(s):
|
||
return s.strip().startswith("//")
|
||
|
||
def split_blocks(s):
|
||
blocks = []
|
||
in_code = True
|
||
block = ""
|
||
for line in s.rstrip().split("\n"):
|
||
if is_comment(line):
|
||
if in_code:
|
||
blocks.append(block)
|
||
block = line + "\n"
|
||
in_code = False
|
||
else:
|
||
block += line + "\n"
|
||
else:
|
||
if not in_code:
|
||
blocks.append(block)
|
||
block = line + "\n"
|
||
in_code = True
|
||
else:
|
||
block += line + "\n"
|
||
if block:
|
||
blocks.append(block)
|
||
return blocks
|
||
|
||
def get_lines(comment, code, n):
|
||
lines = code.split("\n")
|
||
if len(lines) < n:
|
||
raise Exception(
|
||
f"Error in @code block comment:\n{comment}\n{code}\nNeed {n} lines, but there are only {len(lines)}")
|
||
if len(lines) == n:
|
||
return (code, "")
|
||
else:
|
||
return ("\n".join(lines[:n]), "\n".join(lines[n:]))
|
||
|
||
def parse_blocks(blocks):
|
||
box_comment_rgx = re.compile("\\s*//(\\d+)\\s+.*", re.S)
|
||
i = 0
|
||
block_pairs = []
|
||
while i < len(blocks):
|
||
match = box_comment_rgx.match(blocks[i])
|
||
if match:
|
||
boxed_code, rest_code = get_lines(blocks[i], blocks[i+1], int(match.group(1)))
|
||
block_pairs.append([boxed_code, blocks[i]])
|
||
block_pairs.append([rest_code, ""])
|
||
i += 2
|
||
else:
|
||
block_pairs.append([blocks[i], ""])
|
||
i += 1
|
||
return block_pairs
|
||
|
||
|
||
# Characters that are special to LATEX but not to Klammertext. # and ^ are
|
||
# deliberately ABSENT: they are Klammertext specials and are handled by
|
||
# quote_specials() below, not here -- see tex_line().
|
||
latex_escapes = {
|
||
"\\": "\\textbackslash{}", "{": "\\{", "}": "\\}", "%": "\\%",
|
||
"$": "\\$", "&": "\\&", "_": "\\_", "~": "\\textasciitilde{}",
|
||
"<": "\\textless{}", ">": "\\textgreater{}",
|
||
}
|
||
latex_escape_rgx = re.compile("[" + re.escape("".join(latex_escapes)) + "]")
|
||
|
||
def escape_latex(s):
|
||
r"""Escape LaTeX special characters in code text.
|
||
|
||
ONE pass, not a sequence of str.replace() calls: a sequence corrupts
|
||
the replacements it has already made. Replacing \ first yields
|
||
\textbackslash{}, whose braces the later { and } replacements then
|
||
escape in turn -- so a C++ "\n" typesets as \{}n. A single regex pass
|
||
over the original string cannot revisit its own output.
|
||
"""
|
||
return latex_escape_rgx.sub(lambda m: latex_escapes[m.group()], s)
|
||
|
||
def quote_specials(s):
|
||
r"""Quote the Klammertext special characters in code text.
|
||
|
||
An @eval result is RE-READ as Klammertext, so a special character that
|
||
reaches the result raw is interpreted again: a # in "#include" starts a
|
||
text removal and silently eats the rest of the line. Quoting is the
|
||
fix, and it must be quoting rather than LaTeX escaping -- \# would put
|
||
the # back and be eaten in turn.
|
||
|
||
The quoted forms resolve at final processing: the tex target declares
|
||
^# -> \# and ^^ -> \textasciicircum{} in its :escape list, and any
|
||
other quoted special decodes back to its own character. So the target,
|
||
not this code, decides what a # becomes in LaTeX.
|
||
|
||
^ is quoted FIRST, since the other quotings introduce ^ characters.
|
||
"""
|
||
for ch in "^@#|":
|
||
s = s.replace(ch, "^" + ch)
|
||
return s
|
||
|
||
def tex_line(line):
|
||
r"""One code line, ready to be a \klline argument.
|
||
|
||
Three steps in this order: LaTeX-escape the characters only LaTeX cares
|
||
about; quote the ones Klammertext would re-interpret (the escapes above
|
||
introduce none of them, so the two passes cannot interfere); then make
|
||
every space a ~ -- a non-breaking space, exactly one character wide in
|
||
a monospace font, which LaTeX will not collapse -- so the code's
|
||
indentation and internal alignment survive.
|
||
"""
|
||
# An apostrophe is hidden as a marker: after a quoted caret it would
|
||
# otherwise read back as "^" + "^'", the opening of a literal span, and
|
||
# "@c \^' c@" -- the regex of that very katom -- failed the document
|
||
# with a span that does not end. html_line hides it with the other
|
||
# typographic characters; the marker decodes to the plain character.
|
||
quoted = quote_specials(escape_latex(line)).replace("'", klammer_base.escape_ktesc("'"))
|
||
return quoted.replace(" ", "~")
|
||
|
||
def widest_line(lines):
|
||
r"""The line to measure the block's width with.
|
||
|
||
Measured on the RAW lines: in a monospace font character count is
|
||
exact, whereas the escaped text is longer than it typesets
|
||
(\textbackslash{} is eleven characters and one glyph). The chosen
|
||
line is escaped afterwards, for \settowidth.
|
||
"""
|
||
return max(lines, key=len) if lines else ""
|
||
|
||
def tex_block(code, comment):
|
||
r"""One block: its lines, shaded if the block is annotated, beside its
|
||
comment. See the "Annotated code listings" section of sty/code.sty,
|
||
which owns the layout; this only supplies the three arguments."""
|
||
lines = code.strip("\n").split("\n")
|
||
if not any(line.strip() for line in lines):
|
||
return ""
|
||
# Every line is a plain \klline; whether the BLOCK is shaded is decided
|
||
# by \klcodebox from the comment, because the shading is one box around
|
||
# the block (that is what gives it vertical padding).
|
||
body = "\\\\\n".join(f"\\klline{{{tex_line(line)}}}" for line in lines)
|
||
return ("\\klblock{" + tex_line(widest_line(lines)) + "}{%\n"
|
||
+ body + "}{" + comment + "}\n")
|
||
|
||
def comment_text(comment):
|
||
"""The prose of a block comment: the // and the line count removed."""
|
||
return re.sub(r"^\s*//\d*\s*", "", comment.strip(), flags=re.S).strip()
|
||
|
||
html_escapes = {"&": "&", "<": "<", ">": ">"}
|
||
html_escape_rgx = re.compile("[" + re.escape("".join(html_escapes)) + "]")
|
||
|
||
def html_line(text):
|
||
"""Code text as html: the markup characters escaped, then the
|
||
Klammertext specials quoted for the @eval read-back (same reason as
|
||
tex_line -- a raw # in "#include" would start a text removal). The
|
||
html entities introduce no Klammertext special, so the two passes
|
||
cannot interfere. No target :escape entries apply here, so each
|
||
quoted special decodes back to its own character.
|
||
|
||
Last, the typographically active characters are hidden as KTESC
|
||
markers (klammer_base.hide_typographic): the html target's transforms
|
||
run over the final result, and without this a "--check" in a listing
|
||
became an en-dash. The markers decode after the transform pass.
|
||
tex_line needs no such step -- the tex target declares no typographic
|
||
transforms (LaTeX applies its input conventions itself), and its ~ is
|
||
this code's own markup for a preserved space."""
|
||
return klammer_base.hide_typographic(quote_specials(html_escape_rgx.sub(
|
||
lambda m: html_escapes[m.group()], text)))
|
||
|
||
def html_block(code, comment):
|
||
r"""One block: its lines beside its comment.
|
||
|
||
The layout is CSS (sks/code/css/code.css): .code_block is a flex row
|
||
with align-items center -- the same model as the LaTeX \parbox[c]
|
||
pair -- and .code_text is an inline-block with white-space: pre, so
|
||
its shrink-to-fit width IS the block's longest line and the shading
|
||
is one solid rectangle with no per-line work. Nothing is measured
|
||
here: unlike LaTeX, the browser does the layout.
|
||
"""
|
||
lines = code.strip("\n").split("\n")
|
||
if not any(line.strip() for line in lines):
|
||
return ""
|
||
shade = "code_border" if comment else "code_no_border"
|
||
result = f'<div class="code_text {shade}">{html_line(code.strip(chr(10)))}</div>'
|
||
if comment:
|
||
result += f'<div class="code_comment">{comment}</div>'
|
||
return f'<div class="code_block">{result}</div>\n'
|
||
|
||
|
||
def expand_whitespace_markers(text, K=None):
|
||
def count(count_match):
|
||
count = int(count_match) if count_match else 1
|
||
return count
|
||
def replace_spaces(match):
|
||
return " " * count(match.group(1))
|
||
def replace_newlines(match):
|
||
return "\n" * count(match.group(1))
|
||
result = text
|
||
result = re.compile(r" *#- *", re.S).sub("", result)
|
||
result = re.compile(r" *#\+(\d*) *", re.S).sub(replace_spaces, result)
|
||
result = re.compile(r"\s*\#\/(\d*)\s*", re.S).sub(replace_newlines, result)
|
||
return result
|
||
|
||
class Code(klammer_base.Klammer_base):
|
||
id = 0
|
||
def __init__(self, K):
|
||
super().__init__(K)
|
||
self.text = expand_whitespace_markers(self.text)
|
||
|
||
def annotated(self, pairs):
|
||
"""Does any block of this listing carry a comment?"""
|
||
return any(comment.strip() for _, comment in pairs)
|
||
|
||
def tex(self):
|
||
# No table: comments sit a fixed distance from their own block and
|
||
# deliberately do not align with each other, and each block's box is
|
||
# as wide as that block's longest line — so there is no column to
|
||
# align and nothing for a table to do. The layout is in
|
||
# sty/code.sty; see its "Annotated code listings" section.
|
||
#
|
||
# This is a RENDERER (final LaTeX, no klammers in the result), so
|
||
# the Klammermachine leaves it alone. It must never emit @code:
|
||
# a klammer that generates itself re-enters its own body with no
|
||
# base case, which the depth guard catches at 200 levels.
|
||
pairs = parse_blocks(split_blocks(self.text))
|
||
blocks = "".join(tex_block(code, comment_text(comment))
|
||
for code, comment in pairs)
|
||
listing = "\\begin{klcode}\n" + blocks + "\\end{klcode}\n"
|
||
captioned = bool(self.number or self.caption)
|
||
annotated = self.annotated(pairs)
|
||
inset = L.offset_length(self.offset)
|
||
|
||
# A caption does NOT by itself require a box, and boxing a listing
|
||
# costs page breaking -- a minipage cannot break, and listings are
|
||
# often long. Only two things genuinely need the shared box that
|
||
# add_caption builds:
|
||
#
|
||
# * a caption BESIDE the listing (:caption_side left or right),
|
||
# which has to know the listing's width;
|
||
# * an unannotated listing that must be centered or right-aligned,
|
||
# which has to be measured before it can be moved.
|
||
#
|
||
# Everything else is placed unboxed, and keeps breaking: the offset
|
||
# becomes \klshift/\klindent inside the environment, and the caption
|
||
# becomes a paragraph above or below, indented and width-matched to
|
||
# the listing. An annotated listing never needs measuring -- it
|
||
# spans the text column by construction.
|
||
beside = captioned and self.caption_side in ("left", "right")
|
||
must_measure = not annotated and (self.hpos != "left" or inset != "0pt")
|
||
if beside or must_measure:
|
||
return L.block(self.tex_boxed(listing, pairs, annotated,
|
||
captioned, inset))
|
||
return L.block(self.tex_unboxed(listing, annotated, captioned, inset))
|
||
|
||
def tex_boxed(self, listing, pairs, annotated, captioned, inset):
|
||
r"""The listing as a box: placed and captioned like a table or an
|
||
image, at the cost of not breaking across pages."""
|
||
measure = ""
|
||
if annotated:
|
||
# It spans the text column, so an offset narrows it rather than
|
||
# moving it; a centered element has no margin to be inset from.
|
||
width = ("\\linewidth" if self.hpos == "center"
|
||
else f"\\dimexpr\\linewidth-{inset}\\relax")
|
||
else:
|
||
# The \settowidth must stay OUTSIDE the minipage it sizes.
|
||
lines = [line for code, _ in pairs
|
||
for line in code.strip("\n").split("\n")]
|
||
measure = ("\\settowidth{\\kllistingwidth}{\\ttfamily "
|
||
+ tex_line(widest_line(lines)) + "}\n")
|
||
width = "\\kllistingwidth"
|
||
boxed = L.minipage(listing, width, vertical="t", center=False)
|
||
if captioned:
|
||
# add_caption attaches the caption to the box and hands the pair
|
||
# to caption_wrapper, so :pos and :offset move both together.
|
||
return measure + L.add_caption(
|
||
boxed, "Listing", self.number, self.caption, width,
|
||
hpos=self.hpos, side=self.caption_side,
|
||
font_symbol=self.caption_style,
|
||
font_size=self.caption_size, offset=self.offset)
|
||
return measure + L.caption_wrapper(boxed, self.hpos,
|
||
offset=self.offset)
|
||
|
||
def tex_unboxed(self, listing, annotated, captioned, inset):
|
||
r"""The listing in the running vertical list, so it can break across
|
||
pages. The offset is \leftskip plus a matching reduction of the
|
||
width \klblock computes its comment column from; the caption is a
|
||
paragraph on the same indent and width."""
|
||
shift = inset if self.hpos == "left" else "0pt"
|
||
# \hpos center cannot move a full-width listing, so it takes no
|
||
# offset -- the same rule the boxed path and caption_wrapper use.
|
||
indent = "0pt" if (annotated and self.hpos == "center") else inset
|
||
setup = (f"\\setlength{{\\klshift}}{{{shift}}}"
|
||
f"\\setlength{{\\klindent}}{{{indent}}}\n")
|
||
result = setup + listing
|
||
if captioned:
|
||
caption = L.make_caption_text(
|
||
self.number, "Listing", self.caption,
|
||
self.caption_style, self.caption_size)
|
||
# \nobreak: a caption must not be separated from its listing by
|
||
# a page break, even though the listing itself may break.
|
||
block = (f"\\noindent\\hspace*{{{shift}}}"
|
||
f"\\parbox[t]{{\\dimexpr\\linewidth-{indent}\\relax}}"
|
||
f"{{{caption}}}\\par")
|
||
if self.caption_side == "top":
|
||
result = block + "\\nobreak\n" + result
|
||
else:
|
||
result = result + "\\nobreak\n" + block + "\n"
|
||
return result
|
||
|
||
def html(self):
|
||
pairs = parse_blocks(split_blocks(self.text))
|
||
result = "".join(html_block(code, comment_text(comment))
|
||
for code, comment in pairs)
|
||
# The same two cases as tex_place, with the browser doing the work.
|
||
# An annotated listing's rows must stay full width so the comment
|
||
# column's flex: 1 has a remainder to take; an unannotated one gets
|
||
# width: fit-content, which is what lets the position container
|
||
# center or right-align it.
|
||
listing = E("div").cls("code_listing").body(result)
|
||
if not self.annotated(pairs):
|
||
listing.cls("code_listing_box")
|
||
# As in tex: the caption goes through the shared helper, so it sits
|
||
# in the same position container as the listing and moves with it.
|
||
if self.number or self.caption:
|
||
# A left-placed (or flowing) listing's caption is flush with it,
|
||
# as in tex, where such a listing is unboxed and its caption a
|
||
# paragraph on the same indent; a centred or right-placed one is
|
||
# boxed there and its caption centred over the box, which is
|
||
# the helper's default here too.
|
||
align = "left" if self.hpos in ("left", "none") else None
|
||
return html_util.add_caption(
|
||
listing, "Listing", self.number, self.caption,
|
||
self.caption_style, self.hpos, self.caption_side, True,
|
||
self.caption_size, offset=self.offset, caption_align=align)
|
||
return html_util.hpos_container(listing, self.hpos, self.offset).str()
|
||
|
||
def txt_indent(self):
|
||
# The block's indent in columns, by @table's rule: a left- or
|
||
# right-placed block is inset by its :offset (0 = flush), ":hpos
|
||
# none" flows with no indent, and "center" -- not attempted in plain
|
||
# text -- keeps the block convention, Target_txt_indent.
|
||
if self.hpos in ("left", "right"):
|
||
return int(kutil.parse_width("txt", self.offset)[0])
|
||
if self.hpos == "none":
|
||
return 0
|
||
return int(self.Target_txt_indent)
|
||
|
||
def txt(self):
|
||
# The listing is LAID OUT here, line by line, and says so with the
|
||
# verbatim marker (phases.txt_lay_out) so the justification pass
|
||
# leaves it alone -- a blank line in the code included, which holds
|
||
# a marker and therefore does not end the paragraph.
|
||
#
|
||
# Each line is prepared exactly as txt()'s inline sibling prepares a
|
||
# fragment: the Klammertext specials QUOTED, because this result is
|
||
# re-read as Klammertext (raw, a "#" removed the rest of its line and
|
||
# a "code@" in a quoted source file closed the document's own span:
|
||
# "A named end katom does not match"), and the typographic characters
|
||
# hidden, so "--x" stays two hyphens. Spaces become the nbsp marker
|
||
# AFTER that, not "~": hide_typographic has already hidden the
|
||
# code's own tildes, and a "~" written here would be turned into a
|
||
# space by the txt target's transform only as long as it survived.
|
||
lines = self.text.strip("\n").split("\n")
|
||
indent = txt_util.NBSP * self.txt_indent()
|
||
def prepared(line):
|
||
line = klammer_base.hide_typographic(quote_specials(line.rstrip()))
|
||
return line.replace(" ", txt_util.NBSP)
|
||
width = max(len(line.rstrip()) for line in lines)
|
||
rule = indent + klammer_base.hide_typographic("-" * width)
|
||
body = [indent + prepared(line) for line in lines]
|
||
# A caption above the listing is ruled off by the caption pass, so
|
||
# the listing draws its own top rule only when nothing does.
|
||
captioned = self.number or self.caption
|
||
caption_above = captioned and self.caption_side in ("top", "left")
|
||
if not caption_above:
|
||
body.insert(0, rule)
|
||
body.append(rule)
|
||
result = phases.txt_lay_out("\n".join(body))
|
||
return txt_util.add_caption(
|
||
result, "Listing", self.number, self.caption, self.caption_side,
|
||
self.txt_indent())
|
||
|
||
|
||
class Code_fragment(klammer_base.Klammer_base):
|
||
def __init__(self, K):
|
||
super().__init__(K)
|
||
|
||
# code_text is a LITERAL parameter (sks/code/code.k), so its content
|
||
# reaches here exactly as written and NOTHING has escaped it -- the
|
||
# machine's target-character pass does not touch literal content. Both
|
||
# methods must therefore escape it themselves, with the same helpers the
|
||
# block form uses on its lines. Before 2026-08-16 the parameter was an
|
||
# ordinary string and the machine did the escaping; html() got away with
|
||
# handling "<" by hand and tex() with nothing at all.
|
||
def html(self):
|
||
return f'<span class="code">{html_line(self.code_text.strip())}</span>'
|
||
|
||
def tex(self):
|
||
return f"{{\\tt {tex_line(self.code_text.strip())}}}"
|
||
|
||
def txt(self):
|
||
# Plain text has no face to switch to: the fragment is its own text.
|
||
# As in html_line/tex_line, the Klammertext specials are quoted for
|
||
# the @eval read-back (a bare "@" in the result would be a klammer
|
||
# without a beginning) and the typographically active characters
|
||
# hidden, so a "--" in code stays two hyphens.
|
||
result = klammer_base.hide_typographic(quote_specials(self.code_text.strip()))
|
||
|
||
# If *nd* ("no delimiters) is false (the default), surround code symbols
|
||
# with the characters ‹ and ›. There are conflicts in Swiss German,
|
||
# Italian and some Scandinavian languages, but the meaning should be
|
||
# clear in context.
|
||
if not self.nd:
|
||
result = "\u2039" + result + "\u203A"
|
||
return result
|
||
|
||
def extract_marked_region(src, marker, filename):
|
||
"""The region of `src` between two lines that are exactly "//<marker>".
|
||
|
||
The delimiter lines must consist SOLELY of "//" + marker and start in the
|
||
first column, so a marker cannot be matched inside indented code or in a
|
||
trailing comment. Both delimiters are the same text: the source brackets a
|
||
region rather than naming a start and a separate end.
|
||
|
||
A marker that is missing, or that appears only once, is an ERROR -- the
|
||
document asked for a region the file does not offer, and silently listing
|
||
the whole file (or nothing) would let the document drift from the code it
|
||
claims to quote, which is the one thing this option exists to prevent.
|
||
"""
|
||
delimiter = "//" + marker
|
||
lines = src.split("\n")
|
||
at = [i for i, line in enumerate(lines) if line == delimiter]
|
||
if len(at) < 2:
|
||
found = "once" if len(at) == 1 else "not at all"
|
||
raise Exception(
|
||
f'The marker "{marker}" appears {found} in "{filename}".\n'
|
||
f' A marked region is bracketed by TWO lines that are exactly\n'
|
||
f' "{delimiter}", each beginning in the first column.')
|
||
if len(at) > 2:
|
||
raise Exception(
|
||
f'The marker "{marker}" appears {len(at)} times in "{filename}"\n'
|
||
f' (lines {", ".join(str(i + 1) for i in at)}); a region needs exactly two.')
|
||
region = lines[at[0] + 1:at[1]]
|
||
while region and not region[0].strip():
|
||
region.pop(0)
|
||
while region and not region[-1].strip():
|
||
region.pop()
|
||
return "\n".join(region)
|
||
|
||
|
||
class Source(Code):
|
||
r"""@source_listing -- a Code listing whose text comes from a FILE.
|
||
|
||
It IS a Code: @source_listing and @code differ only in where the text
|
||
comes from, so they must render identically, and subclassing is what
|
||
guarantees that rather than a second implementation that drifts.
|
||
|
||
It rendered separately until 2026-08-16, and was wrong in a way nothing
|
||
caught: html() quoted only "@" and tex() wrapped the raw text in a
|
||
verbatim environment. An @eval result is RE-READ as Klammertext, so an
|
||
unquoted "#" starts a text removal -- and since the html path had already
|
||
joined the source into one line, a file beginning "#include" produced an
|
||
EMPTY LISTING and exited 0. quote_specials() in this module documents
|
||
exactly that hazard, and this was the one place not using it. Inheriting
|
||
Code's rendering fixes both targets at once: tex_line()/html_line() quote
|
||
the Klammertext specials AND escape the target's own, so the author of the
|
||
source file needs to know about neither.
|
||
|
||
A verbatim environment could not have been made correct here, incidentally:
|
||
a quoted "^#" resolves to "\#" through the tex target's escape list, which
|
||
inside verbatim would print as "\#" rather than "#".
|
||
"""
|
||
def __init__(self, K):
|
||
# Klammer_base, NOT Code: Code's constructor expands whitespace markers
|
||
# in self.text, and there is no "text" parameter here -- the text does
|
||
# not exist until the file has been read. File content carries no
|
||
# whitespace markers anyway, since those come from the katomizer.
|
||
klammer_base.Klammer_base.__init__(self, K)
|
||
# The filename arrives with the target's escapes as markers -- under
|
||
# tex an underscore is one -- and a marker names no file.
|
||
self.filename = klammer_base.unescape_ktesc(self.filename)
|
||
try:
|
||
with open(self.filename) as fp:
|
||
text = fp.read()
|
||
except OSError as e:
|
||
raise Exception(
|
||
f'Cannot read the source listing "{self.filename}": {e.strerror}.\n'
|
||
f' A relative name resolves against the document\'s directory.')
|
||
if self.marker:
|
||
text = extract_marked_region(text, self.marker, self.filename)
|
||
self.text = text
|