def parse_inline_link(ctx: CTX, mark: str, location: Location, content: Content, indentation: int) -> int: if mark != link_text_begin_mark: return PASS with ctx.using_stop_mark(link_text_end_mark): # It uses the original indentation # so the paragraph can be continued. contents = parse_inline(ctx, content.get_location(), content, indentation) if not content.pull(link_text_end_mark): raise StxError(f'Expected mark: {link_text_end_mark}') if content.pull(link_ref_begin_mark): out = StringIO() while not content.pull(link_ref_end_mark): c = content.peek() if c is None: raise StxError(f'Expected {link_ref_end_mark}', content.get_location()) out.write(c) content.move_next() reference = out.getvalue() else: reference = None ctx.composer.add(LinkText(location, contents, reference)) return CONSUMED
def generate_styled_text(parent: Tag, styled_text: StyledText): if styled_text.style == 'strong': tag_name = 'strong' elif styled_text.style == 'emphasized': tag_name = 'em' elif styled_text.style == 'code': tag_name = 'code' elif styled_text.style == 'deleted': tag_name = 'del' else: tag_name = None if tag_name is not None: styled_tag = append_component_tag(parent, styled_text, tag_name) generate_components(styled_tag, styled_text.contents) else: if styled_text.style == 'double-quote': parent.append_literal('“') generate_components(parent, styled_text.contents) parent.append_literal('”') elif styled_text.style == 'single-quote': parent.append_literal('‘') generate_components(parent, styled_text.contents) parent.append_literal('’') else: raise StxError(f'Not supported style: {styled_text.style}.', styled_text.location)
def parse_directive(ctx: CTX, mark: str, location: Location) -> int: if mark != directive_special_mark: return PASS content = ctx.reader.get_content() file_path = content.file_path entry = parse_entry(content) key = entry.name value = entry.value if content.column > 0: content.expect_end_of_line() if key == 'title': ctx.document.title = value.to_str() elif key == 'author': ctx.document.author = value.to_str() elif key == 'format': ctx.document.format = value.to_str() elif key == 'encoding': ctx.document.encoding = value.to_str() elif key == 'stylesheets': ctx.document.stylesheets = value.to_list() elif key == 'include': process_import(ctx, location, file_path, value) elif key == 'output': process_output(ctx, location, value) elif key == 'grammar': process_grammar(ctx, location, value) else: raise StxError(f'Unsupported directive: {key}') return CONSUMED
def add(self, component: Component): if not self.attributes_buffer.empty(): component.apply_attributes(self.attributes_buffer) unknown_attr_keys = self.attributes_buffer.unknown_keys() if len(unknown_attr_keys) > 0: # TODO improve error message raise StxError(f'Unknown attributes: {unknown_attr_keys} ' f'for {type(component)}.') self.attributes_buffer.clear() if len(self.pre_captions) > 0: if isinstance(component, Table): component.caption = self.pre_captions.pop() self.components.append(component) else: figure = Figure(component.location, component, self.pre_captions.pop()) self.components.append(figure) else: self.components.append(component)
def expect_char(self, options: List[str]): c = self.read_next() if c not in options: raise StxError(f'Expected any of {options}') return c
def parse_inline_style(ctx: CTX, mark: str, location: Location, content: Content, indentation: int) -> int: if mark == strong_begin_mark: end_mark = strong_end_mark style = 'strong' elif mark == emphasized_begin_mark: end_mark = emphasized_end_mark style = 'emphasized' elif mark == code_begin_mark: end_mark = code_end_mark style = 'code' elif mark == deleted_begin_mark: end_mark = deleted_end_mark style = 'deleted' elif mark == d_quote_begin_mark: end_mark = d_quote_end_mark style = 'double-quote' elif mark == s_quote_begin_mark: end_mark = s_quote_end_mark style = 'single-quote' else: return PASS with ctx.using_stop_mark(end_mark): # It uses the original indentation # so the paragraph can be continued. contents = parse_inline(ctx, content.get_location(), content, indentation) if not content.pull(end_mark): raise StxError(f'Expected mark: {end_mark}') ctx.composer.add(StyledText(location, contents, style)) return CONSUMED
def register_type(format_key: str, handler_type: Type[OutputAction], override=False): if not override and format_key in _handler_types: raise StxError(f'Output handler already registered: {format_key}') _handler_types[format_key] = handler_type
def check_unknown_options(options: Union[dict, Value], call: FunctionCall): if isinstance(options, dict): unknown_options = options.keys() if len(unknown_options) > 0: raise StxError(f'Unknown options for function {see(call.key)}:' f' {see(unknown_options)}.') elif isinstance(options, Value): options_any = options.to_any() if options_any is not None: raise StxError( f'Unknown options for function {see(call.key)}:' f' {see(options_any)}.', call.location) else: raise StxError(f'Unknown options for function {see(call.key)}.')
def parse_literal(ctx: CTX, mark: str, location: Location, content: Content, indentation_before_mark: int) -> int: if mark != literal_area_mark: return PASS content.read_spaces() function_location = content.get_location() function = try_parse_entry(content) content.expect_end_of_line() out = StringIO() while True: line = content.read_line(indentation_before_mark) if line is None: raise StxError(f'Expected: {mark}', content.get_location()) elif line.startswith(escape_char): line = line[1:] # remove escape char if not line.startswith(mark) and not line.startswith(escape_char): raise StxError(f'Invalid escaped sequence, expected:' f' {see(mark)} or {see(escape_char)}.') elif line.rstrip() == mark: break out.write(line) text = out.getvalue() if function is not None: component = FunctionCall( function_location, inline=False, key=function.name, options=function.value, argument=Literal(location, text), ) else: component = Literal(location, text) ctx.composer.add(component) return CONSUMED
def capture(ctx: CTX): ctx.document.content = capture_component(ctx, indentation=0, breakable=False) if ctx.reader.active(): raise StxError('Unexpected content.', ctx.reader.get_location()) # TODO improve error messages if len(ctx.composer.stack) > 0: raise StxError('unclosed components') elif not ctx.composer.attributes_buffer.empty(): raise StxError( f'not consumed attributes: {ctx.composer.attributes_buffer}') elif len(ctx.composer.pre_captions) > 0: raise StxError('not consumed pre captions: ', ctx.composer.pre_captions[0].location)
def expect_end_of_line(self): with self.checkout() as trx: text = self.read_until(['\n'], consume_last=True) if len(text.strip()) > 0: trx.cancel() raise StxError('Expected end of line.') trx.save()
def parse_entry(content: Content) -> Entry: location = content.get_location() entry = try_parse_entry(content) if entry is None: raise StxError('Expected token or entry', location) return entry
def make_output_action(document: Document, location: Location, arguments: Value) -> OutputAction: format_value = arguments.try_token() if format_value is not None: actual_format = format_value.to_str() actual_target = OutputStdOut() actual_theme = None actual_options = Empty() else: args_map = arguments.to_map() format_value = args_map.pop('format', None) target_value = args_map.pop('target', None) theme_value = args_map.pop('theme', None) options_value = args_map.pop('options', None) if len(args_map) > 0: for unknown_key in args_map.keys(): logger.warning(f'Unknown output argument: {see(unknown_key)}', location) if format_value is not None: actual_format = format_value.to_str() else: raise StxError('Expected output format.', location) if target_value is not None: actual_target = OutputTarget.make(document, target_value.to_str()) else: actual_target = OutputStdOut() if theme_value is not None: actual_theme = theme_value.to_str() else: actual_theme = None if options_value is not None: actual_options = options_value else: actual_options = Empty() handler_type = get_type(actual_format) return handler_type( document=document, location=location, format_key=actual_format, target=actual_target, options=actual_options, theme=actual_theme, )
def process_grammar(ctx: CTX, location: Location, value: Value): d = value.to_dict() lang = d['lang'] file = d['file'] rule = d.get('rule', lang) file = resolve_sibling(ctx.document.source_file, file) try: registry.register_grammar_from_file(lang, file, rule) except Exception as e: raise StxError(str(e), location) from e
def move_next(self): if self.position < self._length: new_line = (self._content[self.position] == LF_CHAR) self.position += 1 if new_line: self.line += 1 self.column = 0 else: self.column += 1 else: raise StxError('EOF')
def resolve_image(document: Document, call: FunctionCall) -> Component: options = utils.make_options_dict(call, 'src') src = options.pop('src', None) alt = options.pop('alt', None) utils.check_unknown_options(options, call) if src is None: raise StxError(f'Missing `src` parameter in image.', call.location) elif not alt: logger.warning('Image without `alt`', call.location) return Image(call.location, src, alt)
def parse_inline(ctx: CTX, location: Location, content: Content, indentation: int) -> List[Component]: ctx.composer.push() try: while not content.halted(): if content.test(ctx.stop_mark): break location = content.get_location() mark = content.read_mark(inline_marks) signal = (parse_inline_function(ctx, mark, location, content) or parse_inline_container(ctx, mark, location, content, indentation) or parse_inline_style(ctx, mark, location, content, indentation) or parse_inline_link(ctx, mark, location, content, indentation) or parse_inline_token(ctx, mark, location, content, indentation) or parse_inline_text(ctx, mark, location, content, indentation)) if signal == PASS: raise StxError(f'Not implemented mark: {mark}') elif signal == CONSUMED: pass elif signal == EXIT: break else: raise AssertionError(f'Illegal signal: {signal}') except StxError as e: raise StxError('Error parsing inline content.', location) from e return ctx.composer.pop()
def parse_inline_container(ctx: CTX, mark: str, location: Location, content: Content, indentation: int) -> int: if mark != container_begin_mark: return PASS with ctx.using_stop_mark(container_end_mark): # It uses the original indentation # so the paragraph can be continued. contents = parse_inline(ctx, content.get_location(), content, indentation) if not content.pull(container_end_mark): raise StxError(f'Expected mark: {container_end_mark}') if not content.pull(function_begin_mark): raise StxError(f'Expected mark: {function_begin_mark}') skip_void(content) function_location = content.get_location() function = parse_entry(content) skip_void(content) if not content.pull(function_end_mark): raise StxError(f'Expected mark: {function_end_mark}') ctx.composer.add( FunctionCall(function_location, inline=True, key=function.name, options=function.value, argument=Composite(location, contents))) return CONSUMED
def resolve_function_call(document: Document, call: FunctionCall): # Check if it is already resolved if call.result is not None: return 0 processor = registry.get(call.key) if processor is None: raise StxError(f'Function not found: {see(call.key)}', call.location) call.result = processor(document, call) if call.result is None: raise call.error('Function call did not produce a component.') return 1 + resolve_component(document, call.result)
def parse_inline_component(ctx: CTX, mark: str, location: Location, content: Content, indentation: int): inlines = parse_inline(ctx, location, content, indentation) if len(inlines) == 0: # TODO return empty component raise StxError('Missing content.', location) elif len(inlines) == 1 and inlines[0].display_mode == DisplayMode.BLOCK: component = inlines[0] else: display_mode = DisplayMode.compute_display_mode(inlines) if display_mode == DisplayMode.BLOCK: component = Composite(location, inlines) else: component = Paragraph(location, inlines) ctx.composer.add(component)
def make_options_dict(call: FunctionCall, key_for_str: Optional[str] = None): options = call.options if key_for_str is not None: options_str = options.try_str() if options_str is not None: return {key_for_str: options_str} value = options.to_any() if value is None: return {} elif isinstance(value, dict): return value raise StxError(f'Unsupported arguments for function {call.key}.', call.location)
def push_post_caption(self, caption: Component): consumed = False for comps in reversed(self.stack): if len(comps) > 0: component = comps[-1] if isinstance(component, Table): component.caption = caption else: figure = Figure(component.location, component, caption) comps[-1] = figure consumed = True break if not consumed: raise StxError('Expected component for post-caption.')
def resolve_embed(document: Document, call: FunctionCall) -> Component: options = utils.make_options_dict(call, 'src') src = options.pop('src', None) utils.check_unknown_options(options, call) if src is None: raise StxError(f'Missing `src` parameter in embed.', call.location) src = resolve_sibling(document.source_file, src) logger.info(f'Embedding: {src}') with open(src, 'r', encoding='UTF-8') as f: text = f.read() # TODO add support for more type of files return Literal(call.location, text, source=src)
def try_parse_group(content: Content): c = content.peek() if c == GROUP_BEGIN_CHAR: content.move_next() skip_void(content) items = parse_values(content) skip_void(content) c = content.peek() if c != GROUP_END_CHAR: raise StxError('Expected group end char', content.get_location()) content.move_next() return Group(items) return None
def parse_inline_function(ctx: CTX, mark: str, location: Location, content: Content) -> int: if mark != function_begin_mark: return PASS skip_void(content) function_location = content.get_location() function = parse_entry(content) skip_void(content) if not content.pull(function_end_mark): raise StxError(f'Expected mark: {function_end_mark}') ctx.composer.add( FunctionCall(function_location, inline=True, key=function.name, options=function.value)) return CONSUMED
def try_parse_token_or_entry( content: Content, allow_entry_separator=True) -> Union[Token, Entry, None]: text = try_parse_text(content) if text is None: return None loc0 = content.get_location() skip_void(content) c = content.peek() if c == ENTRY_SEPARATOR_CHAR and allow_entry_separator: content.move_next() skip_void(content) entry_name = text entry_value = try_parse_item(content, allow_entry_separator=False) if entry_value is None: raise StxError('Expected an entry value', content.get_location()) return Entry(entry_name, entry_value) group = try_parse_group(content) if group is not None: return Entry(text, group) # go before skipping void content.go_back(loc0) return Token(text)
def error(self, message: str) -> Exception: return StxError(message, self.location)
def parse_inline_text(ctx: CTX, mark: str, location: Location, content: Content, indentation: int) -> int: if mark is not None: return PASS out = StringIO() completed = False while content.peek() is not None: # Check if the text is broken by an inline or stop mark if content.test_any(inline_marks): break elif content.test(ctx.stop_mark): break c = content.peek() if c == '\n': out.write(c) content.move_next() # Check if the text is completed by an empty line if content.consume_empty_line(): completed = True break loc0 = content.get_location() spaces = content.read_spaces(indentation) # Check if the text is completed by indentation change if spaces < indentation: content.go_back(loc0) completed = True break # Check if the text is completed by a non-inline mark if content.test_any(not_inline_marks): content.go_back(loc0) completed = True break elif c == escape_char: content.move_next() escaped_mark = content.pull_any(all_marks) if escaped_mark is not None: out.write(escaped_mark) elif content.pull(ctx.stop_mark): out.write(ctx.stop_mark) elif content.pull(escape_char): out.write(escape_char) else: raise StxError('invalid escaped char') else: out.write(c) content.move_next() text = out.getvalue() if text == '': return EXIT ctx.composer.add(PlainText(location, text)) if completed: return EXIT return CONSUMED
def push_attribute(self, key: str, value: Value): if key in self.attributes_buffer.keys(): raise StxError(f'The attribute {see(key)} was already defined.') self.attributes_buffer.put(key, value)
def generate_function_call(parent: Tag, call: FunctionCall): if call.result is None: raise StxError(f'Function {call.key} was not processed.', call.location) generate_component(parent, call.result)