Skip to content

Documents

yex.Document(style=yex.style.Plain) #

The state of the document. All macro definitions, fonts, and so on are kept here.

Mostly, you interact with a Document as if it was a dict, by getting and setting the values of its elements (known as "subscripting"). This makes it clearer and easier when we have to reset values at the end of a TeΧ group.

The names of all elements are strings. The values depend on the element. Some possible names:

  • The name of any predefined control. For example, doc['\if'].
  • The name of any user-defined macro.
  • The name of any register. For example, doc['\count23'] or doc['\box12'].
  • The prefix of any register, such as doc['\count'] You must supply parser, so we can find the rest of it.
  • Some internal special values:
    • doc['_font'], for the current font.
    • doc['_mode'], for the current mode.
  • A few controls can themselves be subscripted. Writing doc['\font3'] is equivalent to writing doc['\font'][3].

    The second subscript must be an integer, and can be negative. You can also separate the field name from the field subscript with a semicolon. So doc['font;3'], doc['font3'], and doc['font'][3] are equivalant. doc['cmr10;3'] couldn't be written without the semicolon.

Attributes:

Name Type Description
created_at datetime.Datetime

when the Document was constructed. This provides initial values for TeΧ's time-based parameters, such as \year.

controls yex.controls.ControlsTable

all the controls defined, both built-in and user-defined.

groups typing.List[yex.document.Group]

the nested groups of the TeΧ source being processed, which are created either by { and } or by \begingroup and \endgroup.

fonts typing.Mapping[str, Font]

fonts currently loaded. They need not have identifiers in the controls table, but they're not accessible from TeΧ code unless they do.

font yex.font.Font

the currently selected font.

mode yex.mode.Mode

the currently selected mode.

output yex.output.Output

the output driver. For example, the PDF driver or the SVG driver.

contents typing.List[yex.box.Box]

the rendered contents waiting to go to the output driver.

parshape typing.List[yex.value.Dimen]

you probably don't need to look at this. It's a list of constraints on lengths of lines in the current paragraph, set by \parshape but kept here so it persists.

ifdepth yex.document.document._Ifdepth_List

essentially a list of booleans, representing whether particular conditional clauses are executing. For example, after \iftrue the top member will be True, after \iffalse it will be False, and \else will (generally) negate the top member.

style yex.style.Style

a stylesheet-- that is, a module which runs a particular file before we read the main document. The usual example is yex.style.Plain, which represents plain.tex.

Source code in yex/document/document.py
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
def __init__(self,
             style:yex.style.Style = yex.style.Plain,
             ):

    self.created_at = datetime.datetime.now()

    self.style = style()

    self.controls = yex.control.ControlsTable(doc=self)
    self.controls |= yex.keyword.handlers()

    self.fonts = {}

    self.groups = []

    self.parshape = None

    self.ifdepth = _Ifdepth_List([True])
    self.call_stack = []

    self.font = yex.font.Font.from_name(
            name=None,
            doc=self,
            )
    self.mode = self.outermost_mode = yex.mode.Vertical(
            doc=self,
            is_outermost=True,
            )

    self.output = None
    self.contents = []

    self.controls |= {
            '_inputs': yex.io.StreamsTable(doc=self,
            our_type=yex.io.InputStream),
            '_outputs': yex.io.StreamsTable(doc=self,
            our_type=yex.io.OutputStream),
            }

    logger.debug("created, with style %s", self.style)

__delitem__(field) #

See delete().

Source code in yex/document/document.py
489
490
491
492
493
494
495
496
497
def __delitem__(self,
                field:str,
        ):
    r"""
    See delete().
    """
    self.delete(
            field = field,
            )

__getitem__(field) #

Retrieves the value of an element of this doc.

The remarks in the docstring for Document.get() about parser=None apply to this method too.

Parameters:

Name Type Description Default
field str

the name of the element to find. See the class description for details of field names.

required

Returns:

Type Description
typing.Any

the value you asked for, hopefully

Raises:

Type Description
KeyError

if there is no element with the name you requested, and default was not specified.

ParseError

if you asked for an array, and we couldn't figure out how to complete the request without a token stream.

Source code in yex/document/document.py
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
def __getitem__(self,
                field:str,
                ) -> Any:
    r"""
    Retrieves the value of an element of this doc.

    The remarks in the docstring for Document.get() about `parser=None`
    apply to this method too.

    Args:
        field: the name of the element to find.
            See the class description for details of field names.

    Returns:
        the value you asked for, hopefully

    Raises:
        KeyError: if there is no element with the name you requested,
            and `default` was not specified.
        ParseError: if you asked for an array, and we couldn't figure out
            how to complete the request without a token stream.
    """
    return self._inner_get(
            field = field,
            )

__iadd__(thing) #

Short for read(thing). See read for more information.

Parameters:

Name Type Description Default
thing str | typing.TextIO

something to read characters from.

required
Source code in yex/document/document.py
190
191
192
193
194
195
196
197
198
def __iadd__(self, thing: (str|TextIO)) -> Self:
    r"""Short for `read(thing)`. See `read` for more information.

        Args:
            thing: something to read characters from.
    """
    self.read(thing)

    return self

__setitem__(field, value) #

See under set().

Source code in yex/document/document.py
200
201
202
203
204
205
206
207
208
209
210
def __setitem__(self,
                field: str,
                value: Any,
                ):
    """
    See under set().
    """
    self._inner_set(
            field = field,
            value = value,
            )

begin_group(**kwargs) #

Opens a new group.

Called by { and \begingroup.

Keyword arguments are passed to the constructor of Group.

Returns a Group; you can usually discard this, but it's also useful to pass to end_group() to make sure the groups are balanced.

Source code in yex/document/document.py
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
def begin_group(self,
        **kwargs,
        ):
    r"""
    Opens a new group.

    Called by `{` and `\begingroup`.

    Keyword arguments are passed to the constructor of Group.

    Returns a Group; you can usually discard this, but it's
    also useful to pass to `end_group()` to make
    sure the groups are balanced.
    """

    new_group = Group(
            doc = self,
            **kwargs,
            )

    self.groups.append(new_group)
    logger.debug("%sStarted group: %s",
            '  '*len(self.groups),
            self.groups)

    return new_group

delete(field) #

Deletes an element, if you can.

In most cases, this removes the named element from the document's controls table. For registers, such as \count23, the deletion is handled by their array, so the meaning may differ. For example, deleting \count23 simply sets its value to zero.

Parameters:

Name Type Description Default
field str

the name of the element to delete. See the class description for details of field names.

required
Source code in yex/document/document.py
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
def delete(self,
           field:str,
              ):
    r"""
    Deletes an element, if you can.

    In most cases, this removes the named element from the
    document's controls table. For registers, such as `\count23`,
    the deletion is handled by their array, so the meaning may
    differ. For example, deleting `\count23` simply sets its
    value to zero.

    Args:
        field: the name of the element to delete.
            See the class description for details of field names.
    """
    logger.debug("doc[%s]: getting value, to delete it",
            repr(field))

    name, index = self._parse_name(field, None)

    if index is None:
        del self.controls[name]
    else:
        del self.controls[name][index]

end_all_groups(parser=None) #

Closes all open groups.

Parameters:

Name Type Description Default
parser typing.Union[Parser, None]

the token stream we're reading. This is only needed if one of the groups we're ending has produced a list which now has to be handled.

None
Source code in yex/document/document.py
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
def end_all_groups(self,
                   parser: Union['Parser', None] = None,
        ) -> None:
    """
    Closes all open groups.

    Args:
        parser: the token stream we're reading.
            This is only needed if one of the groups we're ending
            has produced a list which now has to be handled.
    """
    logger.debug("ending all groups: %s", self.groups)
    while self.groups:
        self.end_group(
                parser=parser,
                )
    logger.debug("=done ending all groups")

end_group(group=None, from_endgroup=None, parser=None) #

Closes a group.

Discards all settings made since the most recent begin_group(), except global settings.

Called by } and \endgroup.

Parameters:

Name Type Description Default
group yex.document.group.Group | None

the group we should be closing. This only functions as a check; we can only close the top group in the stack. If this is None, which is the default, we just close the top group without doing a check.

None
from_endgroup bool | None

if True, we got here from an \end_group command; if False, we got here from a } token; if None, we got here some other way. If this is non-None, it gets matched against the from_begingroup property of the group we're closing.

None
parser typing.Union[yex.parse.Parser, None]

the token stream we're reading. This is only needed if the group we're ending has produced a list which now has to be handled.

This argument can be None, if you're sure that won't happen; if it does, and the handler needs a token stream, you'll get an error from the handler.

None

Raises:

Type Description
`YexError`

if there are no groups remaining.

Source code in yex/document/document.py
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
def end_group(self,
              group:(Group|None)=None,
              from_endgroup:(bool|None)=None,
              parser: Union['yex.parse.Parser', None]=None,
        ):
    r"""
    Closes a group.

    Discards all settings made since the most recent `begin_group()`,
    except global settings.

    Called by `}` and `\endgroup`.

    Args:
        group: the group we should be closing.
            This only functions as a check; we can only close the
            top group in the stack. If this is None, which is the
            default, we just close the top group without doing a check.

        from_endgroup: if True, we got here from an
            `\end_group` command; if False, we got here from a `}`
            token; if None, we got here some other way. If this is
            non-None, it gets matched against the `from_begingroup`
            property of the group we're closing.

        parser: the token stream we're reading.
            This is only needed if the group we're ending has produced
            a list which now has to be handled.

            This argument *can* be `None`, if you're sure that won't
            happen; if it does, and the handler needs a token stream,
            you'll get an error from the handler.

    Raises:
        `YexError`: if there are no groups remaining.
    """

    if not self.groups:
        raise yex.exception.MoreGroupEndedThanBeganError()

    logger.debug("closing %s; from_endgroup==%s",
            self.groups[-1], from_endgroup)

    if (from_endgroup is not None and
            from_endgroup!=self.groups[-1].from_begingroup):

        if self.groups[-1].from_begingroup:
            needed = r'\begingroup'
        else:
            needed = '{'

        if from_endgroup:
            found = r'\endgroup'
        else:
            found = '}'

        raise yex.exception.WrongKindOfGroupError(
                needed = needed,
                found = found,
                )

    if group is not None and self.groups[-1]!=group:
        raise ValueError(
                f"expected to close group {group}, "
                f"but group {self.groups[-1]} is the next one to close.")

    self.groups.pop().run_restores()

get(field, parser=None, default=None) #

Retrieves the value of an element of this doc.

Parameters:

Name Type Description Default
field str

the name of the element to find. See the class description for details of field names.

required
default typing.Any

what to return if there is no such element. If you'd rather get an exception, use __getitem__ instead.

None
parser typing.Union[Parser, None]

used to find an integer index for an array. For example, the count register numbered 23 is named "\count23", but this name is three tokens if you write it in TeΧ: \count, 2, and 3.

Thus if you write

get(field=r'\count', parser=parser)

we read the next characters of the parser. If they were 2 and 3, you would get the value of \count23.

This behaviour is handled by the keyword class, so it's possible that parser=None does something useful. Check the docstring for that class to be sure.

None

Returns:

Type Description
typing.Any

the value you asked for, hopefully. Otherwise, the default you specified

Raises:

Type Description
ParseError

if we attempted to complete the field name with parser, but failed.

Source code in yex/document/document.py
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
def get(self,
        field:str,
        parser: Union['Parser',None]=None,
        default: Any=None,
        ) -> Any:
    r"""
    Retrieves the value of an element of this doc.

    Args:
        field: the name of the element to find.
            See the class description for details of field names.
        default: what to return if there is no such element.
            If you'd rather get an exception, use `__getitem__`
            instead.
        parser: used to find an integer index for an array.
            For example, the count register numbered 23 is named
            `"\count23"`, but this name is three tokens if you write
            it in TeX: `\count`, `2`, and `3`.

            Thus if you write
            ```
            get(field=r'\count', parser=parser)
            ```

            we read the next characters of the parser.
            If they were `2` and `3`, you would get the value
            of `\count23`.

            This behaviour is handled by the keyword class,
            so it's possible that `parser=None` does something
            useful. Check the docstring for that class to be sure.

    Returns:
        the value you asked for, hopefully.
            Otherwise, the default you specified

    Raises:
        ParseError: if we attempted to complete the field name with
            `parser`, but failed.
    """

    try:
        return self._inner_get(
                field = field,
                parser = parser,
                )
    except KeyError:
        return default

get_control(field) #

Retrieves a control from our control table.

This is like doc.controls.get(), except that it understands indexes: get_control('\count23') will get you the register for \count23.

Parameters:

Name Type Description Default
field str

the name of a control, possibly including an index

required

Returns:

Type Description
typing.Any

a control

Raises:

Type Description
KeyError

if there is no such control

Source code in yex/document/document.py
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
def get_control(self,
                field:str,
                ) -> Any:
    r"""
    Retrieves a control from our control table.

    This is like `doc.controls.get()`, except that it understands
    indexes: `get_control('\count23')` will get you the register
    for `\count23`.

    Args:
        field: the name of a control, possibly including an index

    Returns:
        a control

    Raises:
        KeyError: if there is no such control
    """
    return self._inner_get(
            field = field,
            param_control = True,
            )

open(source, subclass=None, **kwargs) #

Opens a string, a list of characters, or a file for reading.

Constructs an Parser on what. All kwargs are passed to the Parser.

Parameters:

Name Type Description Default
source str | list | typing.TextIO

where we're getting the symbols from.

required
Source code in yex/document/document.py
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
def open(self,
         source: (str|list|TextIO),
         subclass = None,
         **kwargs) -> 'yex.parse.Parser':

    r"""Opens a string, a list of characters, or a file for reading.

        Constructs an `Parser` on `what`.
        All kwargs are passed to the `Parser`.

        Args:
            source: where we're getting the symbols from.
        """

    if subclass is None:
        subclass = yex.parse.Parser
    else:
        assert issubclass(subclass, yex.parse.Parser)

    if not isinstance(source, yex.parse.Tokeniser):
        source = yex.parse.Tokeniser(
                doc = self,
                source = source,
                )

    e = yex.parse.Parser(
            source = source,
            **kwargs,
            )
    return e

read(what, **kwargs) #

Reads a string, or a file, and adds it to this Document.

All kwargs are passed to the Parser, which we'll use to parse the input.

Parameters:

Name Type Description Default
what str | typing.TextIO

something to read characters from.

required
Source code in yex/document/document.py
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
def read(self,
         what: (str|TextIO),
        **kwargs) -> None:
    r"""Reads a string, or a file, and adds it to this Document.

        All kwargs are passed to the `Parser`, which we'll
        use to parse the input.

        Args:
            what: something to read characters from.
    """

    logger.debug("reading from %s, with params %s", what, kwargs)

    e = self.open(what, **kwargs)

    logger.debug(">reading through %s", e)

    for item in e:
        logger.debug("resulting in: %s", item)

        if item is None:
            break

        self.mode.handle(
                item=item,
                parser=e,
                )

    logger.debug("<done reading", self)

remember_restore(f, v) #

Stores a record of an assignment, so it can be undone at the end of the current group. Doesn't actually make the assignment. You probably don't want to use this.

Other than changes to internal flags, this just calls through to remember_restore in the topmost :obj:Group.

Parameters:

Name Type Description Default
f str

field name

required
v typing.Any

field value

required

Returns:

Type Description
None

None

Source code in yex/document/document.py
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
def remember_restore(self, f:str, v:Any) -> None:
    r"""
    Stores a record of an assignment, so it can be undone at the end
    of the current group. Doesn't actually make the assignment.
    You probably don't want to use this.

    Other than changes to internal flags, this just calls through
    to `remember_restore` in the topmost :obj:`Group`.

    Args:
        f: field name
        v: field value

    Returns:
        `None`
    """
    if not self.groups:
        return
    self.groups[-1].remember_restore(f,v)

save() #

Renders the document to the output driver specified by doc['_output'].

Ends all open groups before it attempts to render.

Raises:

Type Description
OSError

if something goes wrong during writing

Source code in yex/document/document.py
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
def save(self) -> None:
    """
    Renders the document to the output driver specified
    by `doc['_output']`.

    Ends all open groups before it attempts to render.

    Raises:
        OSError: if something goes wrong during writing
    """

    logger.debug("saving document to %s", self.output)
    self.end_all_groups()

    while not self.mode == self.outermost_mode:
        self.mode.close()

    self.outermost_mode.exercise_page_builder()
    self.paragraphs.flush()

    tracingoutput = self.controls.get(
            r'\tracingoutput',
            param_control=True,
            )

    if not self.output:
        print("note: there was no output driver")
        return

    self.output.render()

set(field, value) #

Assigns a value to an element of this doc.

Parameters:

Name Type Description Default
field str

the name of the element to change. See the class description for a list of field names.

required
value typing.Any

the value to give the element. Acceptable types and values depend on the field name. Passing None is exactly equivalent to calling doc.delete(field).

required

Raises:

Type Description
KeyError

if the field doesn't name an element

TypeError

if the value has the wrong type for the field

ValueError

if there's something wrong with the value

Source code in yex/document/document.py
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
def set(self,
        field: str,
        value: Any,
        ):
    r"""
    Assigns a value to an element of this doc.

    Args:
        field: the name of the element to change.
            See the class description for a list of field names.
        value: the value to give the element.
            Acceptable types and values depend on the field name.
            Passing None is exactly equivalent to calling
            `doc.delete(field)`.

    Raises:
        KeyError: if the field doesn't name an element
        TypeError: if the value has the wrong type for the field
        ValueError: if there's something wrong with the value
    """
    self._inner_set(
            field = field,
            value = value,
            )

set_control(field, value) #

Sets a control in our control table.

This is like doc.controls.get(), except that it understands indexes: set_control('\count23', ...) will set the register for \count23.

Parameters:

Name Type Description Default
field str

the name of a control, possibly including an index

required

Raises:

Type Description
KeyError

if there is no such control

Source code in yex/document/document.py
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
def set_control(self,
        field: str,
        value: 'yex.control.Control',
        ):
    r"""
    Sets a control in our control table.

    This is like `doc.controls.get()`, except that it understands
    indexes: `set_control('\count23', ...)` will set the register
    for `\count23`.

    Args:
        field: the name of a control, possibly including an index

    Raises:
        KeyError: if there is no such control
    """
    self._inner_set(
            field = field,
            value = value,
            param_control = True,
            )

shipout(box) #

Sends a box, or multiple boxes, to the output queue.

Anything passed to this method will be stored, rather than rendered immediately. It will be rendered when the save method is called.

Parameters:

Name Type Description Default
box typing.Union[Box, typing.List[Box]]

a box or boxes to be rendered.

required
Source code in yex/document/document.py
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
def shipout(self, box: Union['Box', List['Box']]) -> None:
    """
    Sends a box, or multiple boxes, to the output queue.

    Anything passed to this method will be stored, rather than
    rendered immediately. It will be rendered when the `save` method
    is called.

    Args:
        box: a box or boxes to be rendered.
    """

    if isinstance(box, list):
        for item in box:
            self.paragraphs.add(item)
    else:
        self.paragraphs.add(box)

showlists() #

Prints details of the list in the current Mode, and of all the containers it contains, and all the containers they contain, and so on.

Implements the \showlists debugging command: see p88 of the TeΧbook.

Currently disabled.

Source code in yex/document/document.py
665
666
667
668
669
670
671
672
673
674
675
676
def showlists(self) -> None:
    r"""
    Prints details of the list in the current `Mode`, and of all
    the containers it contains, and all the containers *they* contain,
    and so on.

    Implements the `\showlists` debugging command:
    see p88 of the TeXbook.

    Currently disabled.
    """
    raise NotYetImplemented()