• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

dg / texy / 33043376865

27 Aug 2026 05:39AM UTC coverage: 96.335% (+1.6%) from 94.771%
33043376865

push

github

dg
examples: a Markdown parser built on the parsing engine

Shows that Engine carries a language other than Texy: a CommonMark parser
scoring 611 of 652 examples (94%) of the official suite, plus a runner so
the claim can be checked rather than believed.

The instructive part is where the phase boundary falls. Block constructs and
most inline ones are patterns with handlers, but emphasis pairing depends on
the whole sequence of delimiter runs in a container, which no match can see.
So the parser only marks the runs and the pairing happens over the AST; that
alone takes emphasis from 56% to 99%. Reference links go through the
transform phase for the same reason a definition may follow its use.

3391 of 3520 relevant lines covered (96.34%)

0.96 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.12
/src/Texy/Modules/BlockModule.php
1
<?php declare(strict_types=1);
2

3
/**
4
 * This file is part of the Texy! (https://texy.nette.org)
5
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
6
 */
7

8
namespace Texy\Modules;
9

10
use Texy;
11
use Texy\Helpers;
12
use Texy\Nodes\BlockNode;
13
use Texy\Nodes\CodeBlockNode;
14
use Texy\Nodes\CommentNode;
15
use Texy\Nodes\SectionNode;
16
use Texy\Parsing\ParseContext;
17
use Texy\Range;
18
use Texy\Syntax;
19
use function strlen;
20
use const ENT_NOQUOTES;
21

22

23
/**
24
 * Processes special blocks (/-- code, html, text, div, etc.).
25
 */
26
final class BlockModule extends Texy\Module
27
{
28
        public function __construct(
1✔
29
                private Texy\Texy $texy,
30
        ) {
31
                $texy->allowed[Syntax::BlockDefault] = true;
1✔
32
                $texy->allowed[Syntax::BlockPre] = true;
1✔
33
                $texy->allowed[Syntax::BlockCode] = true;
1✔
34
                $texy->allowed[Syntax::BlockHtml] = true;
1✔
35
                $texy->allowed[Syntax::BlockText] = true;
1✔
36
                $texy->allowed[Syntax::BlockTexySource] = true;
1✔
37
                $texy->allowed[Syntax::BlockComment] = true;
1✔
38
                $texy->allowed[Syntax::BlockDiv] = true;
1✔
39
                $texy->allowed[Syntax::CodeFenced] = true;
1✔
40
        }
1✔
41

42

43
        public function beforeParse(): void
44
        {
45
                $this->texy->addBlockSyntax(
1✔
46
                        Syntax::Blocks,
1✔
47
                        '~^
48
                                /--++ [ \t]*+                 # opening tag /--
49
                                (.*)                          # content type (1)
50
                                ' . Texy\Patterns::ModifierHAlign . '? # modifier (2)
1✔
51
                                $
52
                                ((?:                         # content (3)
53
                                        \n (?0) |                # recursive nested blocks
54
                                        \n.*+                    # or any content
55
                                )*)
56
                                (?:
57
                                        \n \\\--.* $ |           # closing tag
58
                                        \z                       # or end of input
59
                                )
60
                        ~mUix',
61
                        $this->parse(...),
1✔
62
                );
63

64
                // ```language
65
                $this->texy->addBlockSyntax(
1✔
66
                        Syntax::CodeFenced,
1✔
67
                        '~^
1✔
68
                                (`{3,}) [ \t]*+                     # opening fence (1)
69
                                ([^\n`]*)                           # info string (2)
70
                                $
71
                                ((?: \n (?! \1 `*+ [ \t]*+ $ ) .*+ )*) # content (3): lines that are not a valid closing fence
72
                                (?:
73
                                        \n \1 `*+ [ \t]*+ $ |           # closing fence (same length or longer)
74
                                        \z                              # or end of input
75
                                )
76
                        ~mx',
77
                        $this->parseFenced(...),
1✔
78
                );
79
        }
1✔
80

81

82
        /**
83
         * Parses ```language fenced code blocks; content is verbatim.
84
         * @param  array{string, string, string, string}  $matches
85
         * @param  array{int, int, int, int}  $offsets
86
         */
87
        public function parseFenced(ParseContext $context, array $matches, array $offsets): CodeBlockNode
1✔
88
        {
89
                [, , $mInfo, $mContent] = $matches;
1✔
90

91
                // a Texy modifier may trail the info string: ```php .{file: Foo.php}
92
                $mod = null;
1✔
93
                if ($m = Texy\Regexp::match($mInfo, '~' . Texy\Patterns::Modifier . '$~x', captureOffset: true)) {
1✔
94
                        $mod = Texy\Modifier::parse($m[1][0], $offsets[2] + $m[1][1]);
×
95
                        $mInfo = substr($mInfo, 0, $m[0][1]);
×
96
                }
97

98
                // first word of the info string is the language (GFM)
99
                $language = Texy\Regexp::split(trim($mInfo), '~\s+~', limit: 2)[0];
1✔
100

101
                return new CodeBlockNode(
1✔
102
                        'code',
1✔
103
                        substr($mContent, 1), // drop the separator "\n"; intentional blank lines stay
1✔
104
                        $language === '' ? null : $language,
1✔
105
                        $mod,
106
                        new Range($offsets[0], strlen($matches[0])),
1✔
107
                );
108
        }
109

110

111
        /**
112
         * Parses blocks /--foo
113
         * @param  array{string, string, ?string, string}  $matches
114
         * @param  array{int, int, ?int, int}  $offsets
115
         */
116
        public function parse(ParseContext $context, array $matches, array $offsets): ?BlockNode
1✔
117
        {
118
                [, $mParam, $mMod, $mContent] = $matches;
1✔
119

120
                $mod = Texy\Modifier::parse($mMod, $offsets[2] ?? null);
1✔
121
                $parts = Texy\Regexp::split($mParam, '~\s+~', limit: 2);
1✔
122
                // the opening pattern matches case-insensitively, so /--PRE must mean the same
123
                // as /--pre; only the type is lowercased, a language name keeps its spelling
124
                $blocktype = empty($parts[0]) ? Syntax::BlockDefault : 'block/' . Helpers::toLower($parts[0]);
1✔
125
                $param = empty($parts[1]) ? null : $parts[1];
1✔
126

127
                $content = Helpers::outdent($mContent);
1✔
128
                $range = new Range($offsets[0], strlen($matches[0]));
1✔
129

130
                if ($blocktype === Syntax::BlockCode) {
1✔
131
                        return new CodeBlockNode('code', $content, $param, $mod, $range);
1✔
132

133
                } elseif ($blocktype === Syntax::BlockDefault || $blocktype === Syntax::BlockPre) {
1✔
134
                        return new CodeBlockNode($blocktype === Syntax::BlockPre ? 'pre' : 'default', $content, $param, $mod, $range);
1✔
135

136
                } elseif ($blocktype === Syntax::BlockComment) {
1✔
137
                        return new CommentNode($content, $range);
1✔
138

139
                } elseif ($blocktype === Syntax::BlockHtml) {
1✔
140
                        // html/text blocks don't use outdent - preserve original indentation
141
                        return new CodeBlockNode('html', trim($mContent, "\n"), null, $mod, $range);
1✔
142

143
                } elseif ($blocktype === Syntax::BlockText) {
1✔
144
                        // html/text blocks don't use outdent - preserve original indentation
145
                        return new CodeBlockNode('text', trim($mContent, "\n"), null, $mod, $range);
1✔
146

147
                } elseif ($blocktype === Syntax::BlockDiv) {
1✔
148
                        [$content, $map] = Texy\Parsing\OffsetMap::outdentLines(
1✔
149
                                Texy\Parsing\OffsetMap::linesOf($mContent, $offsets[3]),
1✔
150
                                firstLine: true,
1✔
151
                        );
152
                        if ($content === '') {
1✔
153
                                return null;
×
154
                        }
155
                        $parsed = $context->parseBlock($content);
1✔
156
                        $map->applyTo($parsed);
1✔
157
                        return new SectionNode($parsed, 'div', $mod, $range);
1✔
158

159
                } elseif ($blocktype === Syntax::BlockTexySource) {
1✔
160
                        // Store raw texy content, will be parsed and displayed as HTML source in handler
161
                        $content = Helpers::outdent($mContent);
1✔
162
                        if ($content === '') {
1✔
163
                                return null;
×
164
                        }
165
                        return new CodeBlockNode('texysource', $content, null, $mod, $range);
1✔
166
                }
167

168
                // /--php and friends: a code block in that language. Anything that does not look
169
                // like a language name stays refused and degrades to visible text.
170
                return Texy\Regexp::match($parts[0], '~^[a-z][a-z0-9_+#-]*$~i')
1✔
171
                        ? new CodeBlockNode('code', $content, $parts[0], $mod, $range)
1✔
172
                        : null;
1✔
173
        }
174
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc