from panflute import Element , ListContainer , Inline , Block
from panflute import Cite , Code , Emph , Image , LineBreak , Link , Math , Note , Quoted , RawInline , SmallCaps , SoftBreak , Space , Span , Str , Strikeout , Strong , Subscript , Superscript , Underline
from panflute import BlockQuote , BulletList , Citation , CodeBlock , Definition , DefinitionItem , DefinitionList , Div , Figure , Header , HorizontalRule , LineBlock , LineItem , ListItem , MetaBlocks , MetaBool , MetaInlines , MetaList , MetaMap , MetaString , Null , OrderedList , Para , Plain , RawBlock , Table , TableBody , TableFoot , TableHead
from panflute import TableRow , TableCell , Caption , Doc
from panflute import MetaValue
from typing import Union , Callable
from types import ModuleType
import os
import re
import warnings
import importlib
import json
from . whitespace import NBSP
from . elements import FQuoted
from . context import Group , InlineGroup , BlockGroup
from . util import nullify , import_md
from . context import Context , CommandCallable
from . whitespace import Whitespace , bavlna
from . command import BlockCommand , InlineCommand , CodeCommand , Command , InlineError
from . command_util import handle_command_define , parse_command
from . nop_processor import NOPProcessor , ELCl , DoubleDocError
class TransformProcessor ( NOPProcessor ) :
root_file_path : str
root_highlight_style : str = " default "
_command_modules : list [ tuple [ Union [ dict [ str , CommandCallable ] , ModuleType ] , str ] ] = [ ]
class UnknownElementError ( Exception ) :
" An unknown Element has been passed to the TransformProcessor, probably because panflute introduced a new one. "
pass
def __init__ ( self , root_file_path : str , * args , * * kwargs ) :
self . root_file_path = root_file_path
super ( ) . __init__ ( * args , * * kwargs )
def add_command_module ( self , module : Union [ dict [ str , CommandCallable ] , ModuleType ] , module_name : str = " " ) :
self . _command_modules . append ( ( module , module_name ) )
def get_pretransformers ( self ) - > list [ Callable [ [ ELCl ] , ELCl ] ] :
return super ( ) . get_pretransformers ( ) + [ self . handle_if_attribute , self . handle_ifnot_attribute ]
def handle_if_attribute ( self , e : ELCl ) - > ELCl :
# `if` attribute. Only show this element if flag is set.
if hasattr ( e , " attributes " ) and " if " in e . attributes :
if not self . context . is_flag_set ( e . attributes [ " if " ] ) :
return nullify ( e )
return e
def handle_ifnot_attribute ( self , e : ELCl ) - > ELCl :
# `ifnot` attribute. Only show this element if flag is NOT set
if hasattr ( e , " attributes " ) and " ifnot " in e . attributes :
if self . context . is_flag_set ( e . attributes [ " ifnot " ] ) :
return nullify ( e )
return e
def transform_ListContainer ( self , e : ListContainer ) - > ListContainer :
try :
return super ( ) . transform_ListContainer ( e )
except TypeError as err :
names = [ ]
for el in e :
if hasattr ( el , " attributes " ) and " c " in el . attributes :
names . append ( el . attributes [ " c " ] )
if len ( names ) > 0 :
raise InlineError ( f " The command { ' s ' if len ( names ) > 1 else ' ' } { names [ 0 ] if len ( names ) == 1 else names } was called in an Inline way but returned Block content. Put it in a paragraph alone or execute it as a Div using: \n ::: {{ c= { names [ 0 ] if len ( names ) == 1 else ' <command_name> ' } }} \n ::: " )
else :
raise err
def transform_Doc ( self , e : Doc ) - > Doc :
if self . context is not None :
raise DoubleDocError ( )
self . context = Context ( e , self . root_file_path )
for module , module_name in self . _command_modules :
self . context . add_commands_from_module ( module , module_name )
e . content = self . transform ( e . content )
e . content = [ BlockGroup ( * e . content , context = self . context ) ]
return e
def transform_Quoted ( self , e : Quoted ) - > FQuoted :
e . content = self . transform ( e . content )
quote_styles = {
" cs " : " cs " ,
" en " : " en " ,
" sk " : " cs " ,
None : None
}
return FQuoted ( * e . content , quote_type = e . quote_type , style = quote_styles [ self . context . get_metadata ( " lang " ) ] )
def transform_Image ( self , e : Image ) - > Image :
e . content = self . transform ( e . content )
# OG now has Context so this is not needed per se, but I'm keeping this here for the handling of attribute > context > default value
# Pass down "no-srcset" metadatum as attribute down to images.
if not " no-srcset " in e . attributes :
e . attributes [ " no-srcset " ] = self . context . get_metadata ( " no-srcset " ) if self . context . get_metadata ( " no-srcset " ) is not None else False
return e
def create_Group ( self , * content , new_context : Context , inline : bool = False ) - > Group :
old_context = self . context
self . context = new_context
content = self . transform ( [ * content ] )
self . context = old_context
if inline :
return InlineGroup ( * content , context = new_context )
else :
return BlockGroup ( * content , context = new_context )
def transform_Para ( self , e : Para ) - > Union [ Para , Div ] :
if len ( e . content ) == 1 and isinstance ( e . content [ 0 ] , Span ) :
# If the span turns out to be a command, it might return a Div. We should then replace ourselves with the Div
span = e . content [ 0 ]
span = self . transform ( span )
if isinstance ( span , Div ) :
return span
else :
e . content [ 0 ] = span
return super ( ) . transform_Para ( e )
else :
return super ( ) . transform_Para ( e )
def transform_Div ( self , e : Div ) - > Union [ Div , Group , Null , RawBlock ] :
e . content = self . transform ( e . content )
if " group " in e . classes :
# `.group` class for Divs
# Content of Div is enclosed in a separate context, all attributes are passed as metadata
new_context = Context ( Doc ( ) , self . context . path , self . context , trusted = self . context . trusted )
for attribute , value in e . attributes . items ( ) :
new_context . set_metadata ( attribute , value )
return self . create_Group ( * e . content , new_context = new_context )
if " c " in e . attributes :
# Commands can be called multiple ways, this handles the following syntax:
# :::{c=commandname}
# :::
e = BlockCommand ( * e . content , identifier = e . identifier , classes = e . classes , attributes = e . attributes )
return self . transform ( e )
if " partial " in e . attributes :
# `partial` attribute
# Used to include a file which is executed in a different (child) Context.
if not " type " in e . attributes :
e . attributes [ " type " ] = " md "
if not self . context . trusted : # If we're in an untrusted context, we shouldn't allow inclusion of files outside the PWD.
full_path = os . path . abspath ( self . context . dir + " / " + e . attributes [ " partial " ] )
pwd = os . path . abspath ( " . " )
if os . path . commonpath ( [ full_path , pwd ] ) != os . path . commonpath ( [ pwd ] ) :
return nullify ( e )
text = open ( self . context . dir + " / " + e . attributes [ " partial " ] , " r " ) . read ( )
path = self . context . dir + " / " + e . attributes [ " partial " ]
if e . attributes [ " type " ] == " md " :
includedDoc = import_md ( text )
trusted = True
if " untrusted " in e . attributes and ( e . attributes [ " untrusted " ] == True or e . attributes [ " untrusted " ] == ' True ' ) :
trusted = False
if not self . context . trusted :
trusted = False
return self . create_Group ( * includedDoc . content , new_context = Context ( includedDoc , path , self . context , trusted = trusted ) )
elif e . attributes [ " type " ] in [ " tex " , " html " ] :
return RawBlock ( text , e . attributes [ " type " ] )
if " header_content " in e . classes :
header_content = self . context . get_metadata ( " header_content " )
header_content = [ ] if header_content is None else header_content
header_content . append ( MetaBlocks ( * self . transform ( e . content ) ) )
self . context . set_metadata ( " header_content " , header_content )
return Null ( )
if " footer_content " in e . classes :
footer_content = self . context . get_metadata ( " footer_content " )
footer_content = [ ] if footer_content is None else footer_content
footer_content . append ( MetaBlocks ( * self . transform ( e . content ) ) )
self . context . set_metadata ( " footer_content " , footer_content )
return Null ( )
if " lang " in e . attributes :
warnings . warn ( " To set language in a way formátítko will understand, this Div has to have the `.group` class and be a Group. " , UserWarning )
return e
def transform_Span ( self , e : Span ) - > Span :
e . content = self . transform ( e . content )
if " group " in e . classes :
# `.group` class for Spans
# Content of Span is enclosed in a separate context, all attributes are passed as metadata
new_context = Context ( Doc ( ) , self . context . path , self . context , trusted = self . context . trusted )
for attribute , value in e . attributes . items ( ) :
new_context . set_metadata ( attribute , value )
return self . create_Group ( * e . content , new_context = new_context , inline = True )
if " c " in e . attributes :
# Commands can be called multiple ways, this handles the following syntax:
# []{c=commandname} and
e = InlineCommand ( * e . content , identifier = e . identifier , classes = e . classes , attributes = e . attributes )
return self . transform ( e )
if len ( e . content ) == 1 and isinstance ( e . content [ 0 ] , Str ) :
## Handle special command shorthand [!commandname]{}
if re . match ( r " ^![ \ w.]+$ " , e . content [ 0 ] . text ) :
e = InlineCommand ( identifier = e . identifier , classes = e . classes , attributes = { * * e . attributes , " c " : e . content [ 0 ] . text [ 1 : ] } )
return self . transform ( e )
## Handle import [#ksp_formatitko as ksp]{}, [#ksp_formatitko]{type=module} or [#path/file.md]{type=md}
# Import a python module as commands (type=module, the default) or
# import all metadata from a md file, dropping its contents.
elif re . match ( r " ^#.+$ " , e . content [ 0 ] . text ) :
if not " type " in e . attributes :
e . attributes [ " type " ] = " module "
if e . attributes [ " type " ] == " md " :
importedDoc = import_md ( open ( self . context . dir + " / " + e . content [ 0 ] . text [ 1 : ] , " r " ) . read ( ) )
self . transform ( importedDoc . content )
elif e . attributes [ " type " ] == " module " :
matches = re . match ( r " ^( \ w+)(?: as ( \ w+))?$ " , e . content [ 0 ] . text [ 1 : ] )
if not matches :
raise SyntaxError ( f " ` { e . content [ 0 ] . text [ 1 : ] } `: invalid syntax " )
module = importlib . import_module ( matches . group ( 1 ) )
module_name = matches . group ( 1 ) if matches . group ( 2 ) is None else matches . group ( 2 )
self . context . add_commands_from_module ( module , module_name )
elif e . attributes [ " type " ] == " metadata " :
data = json . load ( open ( self . context . dir + " / " + e . content [ 0 ] . text [ 1 : ] , " r " ) )
key = " " if not " key " in e . attributes else e . attributes [ " key " ]
self . context . import_metadata ( data , key )
else :
raise SyntaxError ( f " ` { e . attributes [ ' type ' ] } `: invalid import type " )
return nullify ( e )
## Handle metadata print [$key1.key2]{}
# This is a shorthand for just printing the content of some metadata.
elif re . match ( r " ^ \ $[ \ w.]+$ " , e . content [ 0 ] . text ) :
val = self . context . get_metadata ( e . content [ 0 ] . text [ 1 : ] , False )
if isinstance ( val , MetaInlines ) :
e = Span ( * val . content )
e = self . transform ( e )
elif isinstance ( val , MetaString ) :
e = Span ( Str ( val . string ) )
elif isinstance ( val , MetaBool ) :
e = Span ( Str ( str ( val . boolean ) ) )
else :
raise TypeError ( f " Cannot print value of metadatum ' { e . content [ 0 ] . text [ 1 : ] } ' of type ' { type ( val ) } ' " )
return e
return e
def transform_CodeBlock ( self , e : CodeBlock ) - > Union [ CodeBlock , Div , Null ] :
if " markdown " in e . classes and " group " in e . classes :
includedDoc = import_md ( e . text )
return self . create_Group ( * includedDoc . content , new_context = Context ( includedDoc , self . context . path , self . context , self . context . trusted ) )
if " python " in e . classes and " run " in e . classes :
if not self . context . trusted :
return nullify ( e )
command_output = parse_command ( e . text ) ( BlockCommand ( ) , self . context , self )
e = BlockCommand ( ) . replaceSelf ( * ( [ ] if command_output is None else command_output ) )
return self . transform ( e )
if " python " in e . classes and ( " define " in e . attributes or " redefine " in e . attributes ) :
if not self . context . trusted :
return nullify ( e )
return handle_command_define ( e , self . context )
if " c " in e . attributes :
return self . transform ( CodeCommand ( e . text , identifier = e . identifier , classes = e . classes , attributes = e . attributes ) )
# Pass down metadata 'highlight' and 'highlight_style' as attribute to CodeBlocks
# OG now has Context so this is not needed per se, but I'm keeping this here for the handling of attribute > context > default value
if not " highlight " in e . attributes :
e . attributes [ " highlight " ] = self . context . get_metadata ( " highlight " ) if self . context . get_metadata ( " highlight " ) is not None else True
if not " style " in e . attributes :
e . attributes [ " style " ] = self . context . get_metadata ( " highlight-style " ) if self . context . get_metadata ( " highlight-style " ) is not None else " default "
return e
def transform_Command ( self , e : Command ) - > Union [ Div , Span ] :
if not self . context . get_command ( e . attributes [ " c " ] ) :
raise NameError ( f " Command not defined ' { e . attributes [ ' c ' ] } ' . " )
command_output = self . context . get_command ( e . attributes [ " c " ] ) ( e , self . context , self )
e = e . replaceSelf ( * ( [ ] if command_output is None else command_output ) )
return e
def transform_Whitespace ( self , e : Whitespace ) - > Whitespace :
if bavlna ( e , self . context ) :
return NBSP ( )
else :
return e