@kirigami/php-prepros 1.0.1 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/package.json +5 -5
- package/src/libraries/html.class.php +58 -6
- package/src/libraries/md.class.php +525 -50
- package/src/libraries/yaml.class.php +131 -0
- package/src/prepros.js +52 -33
- package/src/prepros.php +2 -2
package/README.md
CHANGED
|
@@ -53,6 +53,7 @@ Part of the **Kirigami** project ecosystem. Other packages are coming soon.
|
|
|
53
53
|
|
|
54
54
|
---
|
|
55
55
|
|
|
56
|
+
|
|
56
57
|
## How it works
|
|
57
58
|
|
|
58
59
|
`@kirigami/php-prepros` runs your PHP source files inside a **WebAssembly PHP 8.x runtime** ([`@kirigami/php-wasm`](https://github.com/kirigami/php-wasm)), entirely in Node.js — no PHP installation required on the host machine.
|
|
@@ -114,6 +115,9 @@ Source pages live in the directory pointed to by `kirigami.root`. The naming con
|
|
|
114
115
|
|
|
115
116
|
```
|
|
116
117
|
src/
|
|
118
|
+
├── _layout/
|
|
119
|
+
├── _lib/
|
|
120
|
+
├── about/
|
|
117
121
|
├── _index.php → src/index.html
|
|
118
122
|
├── about/
|
|
119
123
|
│ └── _index.php → src/about/index.html
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kirigami/php-prepros",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "PHP preprocessor for the Kirigami static site generator. Compile PHP page templates to clean, deployable HTML — with zero server dependency.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"kirigami",
|
|
@@ -44,14 +44,14 @@
|
|
|
44
44
|
"test": "echo \"Error: no test specified\" && exit 1"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@kirigami/php-wasm": "8.5.
|
|
48
|
-
"
|
|
47
|
+
"@kirigami/php-wasm": "8.5.9-1",
|
|
48
|
+
"@kirigami/struct-walker": "1.0.3"
|
|
49
49
|
},
|
|
50
50
|
"repository": {
|
|
51
51
|
"type": "git",
|
|
52
|
-
"url": "https://github.com/php-kirigami/kirigami"
|
|
52
|
+
"url": "git+https://github.com/php-kirigami/kirigami.git"
|
|
53
53
|
},
|
|
54
|
-
"homepage": "https://github.com/php-kirigami/kirigami
|
|
54
|
+
"homepage": "https://github.com/php-kirigami/kirigami/tree/main/packages/php-prepros#readme",
|
|
55
55
|
"bugs": {
|
|
56
56
|
"url": "https://github.com/php-kirigami/kirigami/issues"
|
|
57
57
|
}
|
|
@@ -101,9 +101,13 @@ class HTML
|
|
|
101
101
|
return "{$pad}<{$tag}{$attrs}></{$tag}>\n";
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
// Si tous les enfants sont inline
|
|
105
|
-
|
|
106
|
-
|
|
104
|
+
// Si tous les enfants sont inline ET que ça ressemble à un flux de texte
|
|
105
|
+
// (du vrai texte, ou un seul élément enfant), on re-sérialise en une seule ligne.
|
|
106
|
+
// Sans looksLikeTextFlow, un <section> qui contient plusieurs gros <a> côte à côte
|
|
107
|
+
// (ex: une grille de logos) se retrouverait aussi collé sur une seule ligne,
|
|
108
|
+
// puisque <a> est dans INLINE — ce n'est pas ce qu'on veut.
|
|
109
|
+
if (static::hasOnlyInlineChildren($node) && static::looksLikeTextFlow($node)) {
|
|
110
|
+
$inner = trim(static::renderInline($node));
|
|
107
111
|
return "{$pad}<{$tag}{$attrs}>{$inner}</{$tag}>\n";
|
|
108
112
|
}
|
|
109
113
|
|
|
@@ -115,6 +119,53 @@ class HTML
|
|
|
115
119
|
return "{$pad}<{$tag}{$attrs}>\n{$inner}{$pad}</{$tag}>\n";
|
|
116
120
|
}
|
|
117
121
|
|
|
122
|
+
// Sérialise le contenu inline d'un nœud sur une seule ligne, en réduisant
|
|
123
|
+
// tout groupe d'espaces/tabs/retours à la ligne du texte source à une seule espace
|
|
124
|
+
// (équivalent au comportement de collapse des espaces en HTML).
|
|
125
|
+
private static function renderInline(Dom\Node $node): string
|
|
126
|
+
{
|
|
127
|
+
$out = '';
|
|
128
|
+
foreach ($node->childNodes as $child) {
|
|
129
|
+
if ($child->nodeType === XML_TEXT_NODE) {
|
|
130
|
+
$text = preg_replace('/\s+/', ' ', $child->nodeValue);
|
|
131
|
+
$out .= htmlspecialchars($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if ($child->nodeType === XML_COMMENT_NODE) {
|
|
135
|
+
$out .= '<!--' . $child->nodeValue . '-->';
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if ($child->nodeType !== XML_ELEMENT_NODE) {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
$tag = strtolower($child->nodeName);
|
|
142
|
+
$attrs = static::renderAttrs($child);
|
|
143
|
+
if (in_array($tag, self::VOID, true)) {
|
|
144
|
+
$out .= "<{$tag}{$attrs}>";
|
|
145
|
+
} else {
|
|
146
|
+
$out .= "<{$tag}{$attrs}>" . static::renderInline($child) . "</{$tag}>";
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return $out;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Distingue "du texte qui contient un peu d'inline" (Cliquez <a>ici</a>.) d'un
|
|
153
|
+
// conteneur qui aligne simplement plusieurs blocs inline côte à côte (grille de <a><img></a>).
|
|
154
|
+
// Vrai si : il y a du texte significatif parmi les enfants, OU un seul enfant élément.
|
|
155
|
+
private static function looksLikeTextFlow(Dom\Node $node): bool
|
|
156
|
+
{
|
|
157
|
+
$elementCount = 0;
|
|
158
|
+
foreach ($node->childNodes as $child) {
|
|
159
|
+
if ($child->nodeType === XML_TEXT_NODE && trim($child->nodeValue) !== '') {
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
if ($child->nodeType === XML_ELEMENT_NODE) {
|
|
163
|
+
$elementCount++;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return $elementCount <= 1;
|
|
167
|
+
}
|
|
168
|
+
|
|
118
169
|
// Vérifie que tous les descendants directs sont inline (texte, void inline, éléments inline)
|
|
119
170
|
// Les éléments inline eux-mêmes ne doivent pas contenir d'éléments block
|
|
120
171
|
private static function hasOnlyInlineChildren(Dom\Node $node): bool
|
|
@@ -165,10 +216,11 @@ class HTML
|
|
|
165
216
|
return $out;
|
|
166
217
|
}
|
|
167
218
|
foreach ($node->attributes as $attr) {
|
|
168
|
-
|
|
169
|
-
|
|
219
|
+
$name = strtolower(trim((string) $attr->name));
|
|
220
|
+
if (in_array($name, self::BOOLEAN_ATTRS, true)) {
|
|
221
|
+
$out .= ' ' . $name;
|
|
170
222
|
} else {
|
|
171
|
-
$out .= ' ' . $
|
|
223
|
+
$out .= ' ' . $name . '="' . htmlspecialchars((string) $attr->value, ENT_QUOTES | ENT_HTML5, 'UTF-8') . '"';
|
|
172
224
|
}
|
|
173
225
|
}
|
|
174
226
|
return $out;
|
|
@@ -64,6 +64,217 @@ class MD {
|
|
|
64
64
|
return array_keys(self::$plugins);
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
// ========================================================================
|
|
68
|
+
// Génère un id de type "slug" pour les ancres de titres (ATX et Setext).
|
|
69
|
+
// ========================================================================
|
|
70
|
+
private static function slugify(string $text): string {
|
|
71
|
+
$id = strtolower(preg_replace('/[^\w\- ]/u', '', $text));
|
|
72
|
+
return preg_replace('/\s+/', '-', trim($id));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ========================================================================
|
|
76
|
+
// Convertit une largeur d'indentation (espaces/tabs) en nombre de colonnes,
|
|
77
|
+
// une tabulation comptant pour 4 espaces.
|
|
78
|
+
// ========================================================================
|
|
79
|
+
private static function indentWidth(string $whitespace): int {
|
|
80
|
+
return strlen(str_replace("\t", ' ', $whitespace));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Construit récursivement une liste (imbriquée) <ol>/<ul> à partir d'un
|
|
85
|
+
* tableau plat d'items { indent, type, text }. $i est avancé au fur et à
|
|
86
|
+
* mesure de la consommation des items.
|
|
87
|
+
*
|
|
88
|
+
* @param array<int, array{indent:int, type:string, text:string}> $items
|
|
89
|
+
*/
|
|
90
|
+
private static function buildListTree(array $items, int &$i, int $count): string {
|
|
91
|
+
$type = $items[$i]['type'];
|
|
92
|
+
$baseIndent = $items[$i]['indent'];
|
|
93
|
+
$out = "<{$type}>\n";
|
|
94
|
+
|
|
95
|
+
while ($i < $count && $items[$i]['indent'] === $baseIndent && $items[$i]['type'] === $type) {
|
|
96
|
+
$text = $items[$i]['text'];
|
|
97
|
+
$i++;
|
|
98
|
+
|
|
99
|
+
$nested = '';
|
|
100
|
+
if ($i < $count && $items[$i]['indent'] > $baseIndent) {
|
|
101
|
+
$nested = "\n" . self::buildListTree($items, $i, $count);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
$out .= " <li>{$text}{$nested}</li>\n";
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return $out . "</{$type}>";
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ========================================================================
|
|
111
|
+
// ÉMOJIS (syntaxe étendue) : :shortcode: → caractère unicode.
|
|
112
|
+
// Table non exhaustive mais couvrant les raccourcis les plus courants ;
|
|
113
|
+
// extensible via registerEmoji().
|
|
114
|
+
// ========================================================================
|
|
115
|
+
/** @var array<string, string> */
|
|
116
|
+
private static array $extraEmoji = [];
|
|
117
|
+
|
|
118
|
+
private static array $emojiMap = [
|
|
119
|
+
'smile' => '😄', 'smiley' => '😃', 'grin' => '😁', 'joy' => '😂', 'rofl' => '🤣',
|
|
120
|
+
'blush' => '😊', 'wink' => '😉', 'relaxed' => '☺️', 'slight_smile' => '🙂',
|
|
121
|
+
'upside_down_face' => '🙃', 'innocent' => '😇', 'heart_eyes' => '😍', 'kissing_heart' => '😘',
|
|
122
|
+
'thinking' => '🤔', 'neutral_face' => '😐', 'expressionless' => '😑', 'no_mouth' => '😶',
|
|
123
|
+
'roll_eyes' => '🙄', 'smirk' => '😏', 'unamused' => '😒', 'grimacing' => '😬',
|
|
124
|
+
'lying_face' => '🤥', 'relieved' => '😌', 'pensive' => '😔', 'sleepy' => '😪',
|
|
125
|
+
'drooling_face' => '🤤', 'sleeping' => '😴', 'mask' => '😷', 'sunglasses' => '😎',
|
|
126
|
+
'star_struck' => '🤩', 'partying_face' => '🥳', 'worried' => '😟', 'frowning' => '☹️',
|
|
127
|
+
'confused' => '😕', 'slightly_frowning_face' => '🙁', 'cry' => '😢', 'sob' => '😭',
|
|
128
|
+
'scream' => '😱', 'confounded' => '😖', 'persevere' => '😣', 'disappointed' => '😞',
|
|
129
|
+
'sweat' => '😓', 'weary' => '😩', 'tired_face' => '😫', 'yawning_face' => '🥱',
|
|
130
|
+
'triumph' => '😤', 'rage' => '😡', 'angry' => '😠', 'cursing_face' => '🤬',
|
|
131
|
+
'exploding_head' => '🤯', 'flushed' => '😳', 'hot_face' => '🥵', 'cold_face' => '🥶',
|
|
132
|
+
'scream_cat' => '🙀', 'nerd_face' => '🤓', 'monocle_face' => '🧐', 'zany_face' => '🤪',
|
|
133
|
+
'raised_eyebrow' => '🤨', 'shushing_face' => '🤫', 'zipper_mouth_face' => '🤐',
|
|
134
|
+
'heart' => '❤️', 'orange_heart' => '🧡', 'yellow_heart' => '💛', 'green_heart' => '💚',
|
|
135
|
+
'blue_heart' => '💙', 'purple_heart' => '💜', 'black_heart' => '🖤', 'white_heart' => '🤍',
|
|
136
|
+
'broken_heart' => '💔', 'two_hearts' => '💕', 'sparkling_heart' => '💖', 'heartbeat' => '💓',
|
|
137
|
+
'thumbsup' => '👍', '+1' => '👍', 'thumbsdown' => '👎', '-1' => '👎',
|
|
138
|
+
'clap' => '👏', 'raised_hands' => '🙌', 'pray' => '🙏', 'wave' => '👋',
|
|
139
|
+
'ok_hand' => '👌', 'v' => '✌️', 'crossed_fingers' => '🤞', 'muscle' => '💪',
|
|
140
|
+
'point_up' => '☝️', 'point_down' => '👇', 'point_left' => '👈', 'point_right' => '👉',
|
|
141
|
+
'handshake' => '🤝', 'writing_hand' => '✍️', 'fire' => '🔥', 'star' => '⭐',
|
|
142
|
+
'star2' => '🌟', 'sparkles' => '✨', 'zap' => '⚡', 'boom' => '💥', 'collision' => '💥',
|
|
143
|
+
'rocket' => '🚀', 'tada' => '🎉', 'confetti_ball' => '🎊', 'gift' => '🎁',
|
|
144
|
+
'balloon' => '🎈', 'trophy' => '🏆', 'medal' => '🏅', 'crown' => '👑',
|
|
145
|
+
'gem' => '💎', 'moneybag' => '💰', 'dollar' => '💵', '100' => '💯',
|
|
146
|
+
'warning' => '⚠️', 'no_entry' => '⛔', 'stop_sign' => '🛑', 'checkered_flag' => '🏁',
|
|
147
|
+
'white_check_mark' => '✅', 'heavy_check_mark' => '✔️', 'x' => '❌', 'negative_squared_cross_mark' => '❎',
|
|
148
|
+
'question' => '❓', 'grey_question' => '❔', 'exclamation' => '❗', 'bangbang' => '‼️',
|
|
149
|
+
'interrobang' => '⁉️', 'bulb' => '💡', 'bell' => '🔔', 'no_bell' => '🔕',
|
|
150
|
+
'lock' => '🔒', 'unlock' => '🔓', 'key' => '🔑', 'mag' => '🔍', 'link' => '🔗',
|
|
151
|
+
'pushpin' => '📌', 'paperclip' => '📎', 'calendar' => '📅', 'clock' => '🕐',
|
|
152
|
+
'hourglass' => '⌛', 'alarm_clock' => '⏰', 'memo' => '📝', 'pencil2' => '✏️',
|
|
153
|
+
'book' => '📖', 'books' => '📚', 'newspaper' => '📰', 'email' => '📧',
|
|
154
|
+
'envelope' => '✉️', 'inbox_tray' => '📥', 'outbox_tray' => '📤', 'package' => '📦',
|
|
155
|
+
'file_folder' => '📁', 'open_file_folder' => '📂', 'clipboard' => '📋',
|
|
156
|
+
'chart_with_upwards_trend' => '📈', 'chart_with_downwards_trend' => '📉', 'bar_chart' => '📊',
|
|
157
|
+
'computer' => '💻', 'desktop_computer' => '🖥️', 'keyboard' => '⌨️', 'printer' => '🖨️',
|
|
158
|
+
'phone' => '📱', 'iphone' => '📱', 'camera' => '📷', 'video_camera' => '📹',
|
|
159
|
+
'tv' => '📺', 'radio' => '📻', 'battery' => '🔋', 'electric_plug' => '🔌',
|
|
160
|
+
'bug' => '🐛', 'beetle' => '🪲', 'gear' => '⚙️', 'wrench' => '🔧', 'hammer' => '🔨',
|
|
161
|
+
'nut_and_bolt' => '🔩', 'toolbox' => '🧰', 'test_tube' => '🧪', 'microscope' => '🔬',
|
|
162
|
+
'satellite' => '🛰️', 'globe_with_meridians' => '🌐', 'earth_americas' => '🌎',
|
|
163
|
+
'sun' => '☀️', 'sunny' => '☀️', 'partly_sunny' => '⛅', 'cloud' => '☁️',
|
|
164
|
+
'rainbow' => '🌈', 'umbrella' => '☂️', 'snowflake' => '❄️', 'droplet' => '💧',
|
|
165
|
+
'ocean' => '🌊', 'tent' => '⛺', 'camping' => '🏕️', 'mountain' => '⛰️',
|
|
166
|
+
'evergreen_tree' => '🌲', 'deciduous_tree' => '🌳', 'palm_tree' => '🌴',
|
|
167
|
+
'cactus' => '🌵', 'seedling' => '🌱', 'four_leaf_clover' => '🍀', 'maple_leaf' => '🍁',
|
|
168
|
+
'dog' => '🐶', 'cat' => '🐱', 'mouse' => '🐭', 'rabbit' => '🐰', 'fox_face' => '🦊',
|
|
169
|
+
'bear' => '🐻', 'panda_face' => '🐼', 'koala' => '🐨', 'tiger' => '🐯', 'lion' => '🦁',
|
|
170
|
+
'cow' => '🐮', 'pig' => '🐷', 'frog' => '🐸', 'monkey_face' => '🐵', 'chicken' => '🐔',
|
|
171
|
+
'penguin' => '🐧', 'bird' => '🐦', 'baby_chick' => '🐤', 'owl' => '🦉',
|
|
172
|
+
'horse' => '🐴', 'unicorn' => '🦄', 'bee' => '🐝', 'butterfly' => '🦋', 'snail' => '🐌',
|
|
173
|
+
'octopus' => '🐙', 'fish' => '🐟', 'dolphin' => '🐬', 'whale' => '🐳',
|
|
174
|
+
'pizza' => '🍕', 'hamburger' => '🍔', 'fries' => '🍟', 'hotdog' => '🌭',
|
|
175
|
+
'taco' => '🌮', 'sushi' => '🍣', 'ramen' => '🍜', 'spaghetti' => '🍝',
|
|
176
|
+
'bread' => '🍞', 'cheese' => '🧀', 'egg' => '🥚', 'popcorn' => '🍿',
|
|
177
|
+
'cookie' => '🍪', 'doughnut' => '🍩', 'cake' => '🍰', 'birthday' => '🎂',
|
|
178
|
+
'candy' => '🍬', 'chocolate_bar' => '🍫', 'icecream' => '🍦', 'apple' => '🍎',
|
|
179
|
+
'banana' => '🍌', 'grapes' => '🍇', 'watermelon' => '🍉', 'strawberry' => '🍓',
|
|
180
|
+
'lemon' => '🍋', 'peach' => '🍑', 'coffee' => '☕', 'tea' => '🍵', 'beer' => '🍺',
|
|
181
|
+
'beers' => '🍻', 'wine_glass' => '🍷', 'cocktail' => '🍸', 'tropical_drink' => '🍹',
|
|
182
|
+
'champagne' => '🍾', 'soccer' => '⚽', 'basketball' => '🏀', 'football' => '🏈',
|
|
183
|
+
'baseball' => '⚾', 'tennis' => '🎾', 'volleyball' => '🏐', 'rugby_football' => '🏉',
|
|
184
|
+
'8ball' => '🎱', 'golf' => '⛳', 'dart' => '🎯', 'video_game' => '🎮',
|
|
185
|
+
'game_die' => '🎲', 'jigsaw' => '🧩', 'car' => '🚗', 'taxi' => '🚕', 'bus' => '🚌',
|
|
186
|
+
'ambulance' => '🚑', 'fire_engine' => '🚒', 'police_car' => '🚓', 'bike' => '🚲',
|
|
187
|
+
'airplane' => '✈️', 'helicopter' => '🚁', 'train' => '🚆', 'ship' => '🚢',
|
|
188
|
+
'house' => '🏠', 'office' => '🏢', 'hospital' => '🏥', 'school' => '🏫',
|
|
189
|
+
'church' => '⛪', 'castle' => '🏰', 'world_map' => '🗺️', 'flag_white' => '🏳️',
|
|
190
|
+
'flag_black' => '🏴', 'checkered_flag2' => '🏁', 'eyes' => '👀', 'eye' => '👁️',
|
|
191
|
+
'speech_balloon' => '💬', 'thought_balloon' => '💭', 'zzz' => '💤', 'boom2' => '💥',
|
|
192
|
+
'sos' => '🆘', 'new' => '🆕', 'ok' => '🆗', 'up' => '🆙', 'cool' => '🆒',
|
|
193
|
+
'free' => '🆓', 'id' => '🆔', 'ng' => '🆖',
|
|
194
|
+
];
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Enregistre (ou remplace) un raccourci emoji personnalisé.
|
|
198
|
+
*/
|
|
199
|
+
public static function registerEmoji(string $shortcode, string $char): void {
|
|
200
|
+
self::$extraEmoji[strtolower(trim($shortcode, ':'))] = $char;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private static function emojiFor(string $shortcode): ?string {
|
|
204
|
+
$key = strtolower($shortcode);
|
|
205
|
+
return self::$extraEmoji[$key] ?? self::$emojiMap[$key] ?? null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ========================================================================
|
|
209
|
+
// LISTES DE DÉFINITION (syntaxe étendue)
|
|
210
|
+
// Terme
|
|
211
|
+
// : Définition
|
|
212
|
+
// Analyse procédurale ligne par ligne (plus sûre qu'une seule grosse
|
|
213
|
+
// regex pour regrouper plusieurs paires terme/définitions dans un même
|
|
214
|
+
// <dl>, séparées ou non par une ligne vide).
|
|
215
|
+
// ========================================================================
|
|
216
|
+
private static function isDefinitionColonLine(string $line): bool {
|
|
217
|
+
return (bool) preg_match('/^[ \t]*:[ \t]+.+$/', $line);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
private static function looksLikeOtherBlock(string $line): bool {
|
|
221
|
+
$t = ltrim($line);
|
|
222
|
+
if ($t === '') return true;
|
|
223
|
+
if (str_starts_with($t, '<')) return true;
|
|
224
|
+
return (bool) preg_match('/^(?:#{1,6}[ \t]|>|```|\||[-*+][ \t]|\d+\.[ \t])/', $t);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private static function extractDefinitionLists(string $html): string {
|
|
228
|
+
$lines = explode("\n", $html);
|
|
229
|
+
$n = count($lines);
|
|
230
|
+
$out = [];
|
|
231
|
+
$i = 0;
|
|
232
|
+
|
|
233
|
+
while ($i < $n) {
|
|
234
|
+
$isTermStart = $i + 1 < $n
|
|
235
|
+
&& !self::looksLikeOtherBlock($lines[$i])
|
|
236
|
+
&& !self::isDefinitionColonLine($lines[$i])
|
|
237
|
+
&& self::isDefinitionColonLine($lines[$i + 1]);
|
|
238
|
+
|
|
239
|
+
if (!$isTermStart) {
|
|
240
|
+
$out[] = $lines[$i];
|
|
241
|
+
$i++;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
$dl = "<dl>\n";
|
|
246
|
+
while (true) {
|
|
247
|
+
$term = trim($lines[$i]);
|
|
248
|
+
$dl .= " <dt>{$term}</dt>\n";
|
|
249
|
+
$i++;
|
|
250
|
+
while ($i < $n && preg_match('/^[ \t]*:[ \t]+(.*)$/', $lines[$i], $m)) {
|
|
251
|
+
$dl .= " <dd>{$m[1]}</dd>\n";
|
|
252
|
+
$i++;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Une seule ligne vide entre deux groupes reste dans le même <dl>
|
|
256
|
+
// si le groupe suivant est bien un nouveau terme.
|
|
257
|
+
if ($i < $n && trim($lines[$i]) === '') {
|
|
258
|
+
$j = $i;
|
|
259
|
+
while ($j < $n && trim($lines[$j]) === '') $j++;
|
|
260
|
+
if ($j + 1 < $n
|
|
261
|
+
&& !self::looksLikeOtherBlock($lines[$j])
|
|
262
|
+
&& !self::isDefinitionColonLine($lines[$j])
|
|
263
|
+
&& self::isDefinitionColonLine($lines[$j + 1])
|
|
264
|
+
) {
|
|
265
|
+
$i = $j;
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
$dl .= "</dl>";
|
|
272
|
+
$out[] = $dl;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return implode("\n", $out);
|
|
276
|
+
}
|
|
277
|
+
|
|
67
278
|
// ========================================================================
|
|
68
279
|
|
|
69
280
|
public static function toHtml(string $markdown): string {
|
|
@@ -137,6 +348,59 @@ class MD {
|
|
|
137
348
|
);
|
|
138
349
|
|
|
139
350
|
|
|
351
|
+
// ====================================================================
|
|
352
|
+
// ÉTAPE 2a : DÉFINITIONS DE NOTES DE BAS DE PAGE (footnotes)
|
|
353
|
+
// [^1]: Texte de la note.
|
|
354
|
+
// [^bignote]: Première ligne.
|
|
355
|
+
//
|
|
356
|
+
// Paragraphe suivant, indenté de 4 espaces ou 1 tabulation.
|
|
357
|
+
//
|
|
358
|
+
// `{ du code }`
|
|
359
|
+
// Extraites (et retirées du texte) AVANT les définitions de liens par
|
|
360
|
+
// référence, car [^label]: matcherait aussi leur regex sinon.
|
|
361
|
+
// Le contenu de chaque note est rendu via un appel récursif à
|
|
362
|
+
// toHtml() pour supporter plusieurs paragraphes, du code, etc.
|
|
363
|
+
// ====================================================================
|
|
364
|
+
$footnoteDefs = [];
|
|
365
|
+
$html = preg_replace_callback(
|
|
366
|
+
'/^\[\^([^\]\s]+)\]:[ \t]?([^\n]*)((?:\n(?:[ \t]{4}[^\n]*|[ \t]*))*)/m',
|
|
367
|
+
function ($m) use (&$footnoteDefs): string {
|
|
368
|
+
$label = strtolower(trim($m[1]));
|
|
369
|
+
$first = $m[2];
|
|
370
|
+
$rest = $m[3] ?? '';
|
|
371
|
+
$restLines = $rest !== '' ? explode("\n", $rest) : [];
|
|
372
|
+
$restLines = array_map(static function (string $l): string {
|
|
373
|
+
return preg_replace('/^(?:[ ]{4}|\t)/', '', $l);
|
|
374
|
+
}, $restLines);
|
|
375
|
+
$content = trim($first . "\n" . implode("\n", $restLines));
|
|
376
|
+
$footnoteDefs[$label] = self::toHtml($content);
|
|
377
|
+
return '';
|
|
378
|
+
},
|
|
379
|
+
$html
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
// ====================================================================
|
|
384
|
+
// ÉTAPE 2b : DÉFINITIONS DE LIENS PAR RÉFÉRENCE
|
|
385
|
+
// [label]: https://example.com "Titre optionnel"
|
|
386
|
+
// [label]: <https://example.com> 'Titre optionnel'
|
|
387
|
+
// [label]: https://example.com (Titre optionnel)
|
|
388
|
+
// Extraites (et retirées du texte) avant tout le reste ; utilisées
|
|
389
|
+
// plus loin par les liens [texte][label] / [texte][].
|
|
390
|
+
// ====================================================================
|
|
391
|
+
$refDefs = [];
|
|
392
|
+
$html = preg_replace_callback(
|
|
393
|
+
'/^[ \t]{0,3}\[([^\]]+)\]:[ \t]*<?([^\s>]+)>?(?:[ \t]+(?:"([^"]*)"|\'([^\']*)\'|\(([^)]*)\)))?[ \t]*$/m',
|
|
394
|
+
function ($m) use (&$refDefs): string {
|
|
395
|
+
$label = strtolower(trim($m[1]));
|
|
396
|
+
$title = $m[3] !== '' ? $m[3] : ($m[4] !== '' ? $m[4] : ($m[5] ?? ''));
|
|
397
|
+
$refDefs[$label] = ['url' => $m[2], 'title' => $title];
|
|
398
|
+
return '';
|
|
399
|
+
},
|
|
400
|
+
$html
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
|
|
140
404
|
// ====================================================================
|
|
141
405
|
// ÉTAPE 3 : BLOCS DE CODE (```lang ... ```)
|
|
142
406
|
// ====================================================================
|
|
@@ -149,8 +413,44 @@ class MD {
|
|
|
149
413
|
return $placeholder;
|
|
150
414
|
}, $html);
|
|
151
415
|
|
|
152
|
-
//
|
|
416
|
+
// ====================================================================
|
|
417
|
+
// ÉTAPE 3a : BLOCS DE CODE INDENTÉS (4 espaces ou 1 tabulation)
|
|
418
|
+
// Reconnu seulement quand précédé d'une ligne vide (ou du début du
|
|
419
|
+
// document) et suivi d'une ligne vide (ou de la fin du document), afin
|
|
420
|
+
// d'éviter les conflits avec l'indentation des listes imbriquées.
|
|
421
|
+
// ====================================================================
|
|
422
|
+
$html = preg_replace_callback(
|
|
423
|
+
'/(?<=\n\n|^)((?:[ ]{4}|\t)[^\n]*(?:\n(?:[ ]{4}|\t)[^\n]*)*)(?=\n\n|\n*$)/',
|
|
424
|
+
function ($matches) use (&$codeBlocks) {
|
|
425
|
+
$lines = explode("\n", $matches[1]);
|
|
426
|
+
$stripped = array_map(static function (string $l): string {
|
|
427
|
+
return preg_replace('/^(?:[ ]{4}|\t)/', '', $l);
|
|
428
|
+
}, $lines);
|
|
429
|
+
$code = htmlspecialchars(implode("\n", $stripped), ENT_QUOTES, 'UTF-8');
|
|
430
|
+
$placeholder = "\x02CB" . count($codeBlocks) . "\x03";
|
|
431
|
+
$codeBlocks[$placeholder] = "<pre><code>{$code}</code></pre>";
|
|
432
|
+
return $placeholder;
|
|
433
|
+
},
|
|
434
|
+
$html
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
// Code inline avec double backticks (permet d'inclure un backtick littéral)
|
|
153
438
|
$inlineCodes = [];
|
|
439
|
+
$html = preg_replace_callback('/``(.+?)``/s', function ($matches) use (&$inlineCodes) {
|
|
440
|
+
$content = $matches[1];
|
|
441
|
+
// Convention standard : si le contenu commence et finit par un
|
|
442
|
+
// espace (et n'est pas uniquement des espaces), on retire un
|
|
443
|
+
// espace de chaque côté — utile pour englober un ` en bordure.
|
|
444
|
+
if (preg_match('/^ (.*[^ ]) $/s', $content, $trim)) {
|
|
445
|
+
$content = $trim[1];
|
|
446
|
+
}
|
|
447
|
+
$code = htmlspecialchars($content, ENT_QUOTES, 'UTF-8');
|
|
448
|
+
$placeholder = "\x02IC" . count($inlineCodes) . "\x03";
|
|
449
|
+
$inlineCodes[$placeholder] = "<code>{$code}</code>";
|
|
450
|
+
return $placeholder;
|
|
451
|
+
}, $html);
|
|
452
|
+
|
|
453
|
+
// Code inline (`...`)
|
|
154
454
|
$html = preg_replace_callback('/`([^`\n]+)`/', function ($matches) use (&$inlineCodes) {
|
|
155
455
|
$code = htmlspecialchars($matches[1], ENT_QUOTES, 'UTF-8');
|
|
156
456
|
$placeholder = "\x02IC" . count($inlineCodes) . "\x03";
|
|
@@ -160,7 +460,57 @@ class MD {
|
|
|
160
460
|
|
|
161
461
|
|
|
162
462
|
// ====================================================================
|
|
163
|
-
// ÉTAPE 3b :
|
|
463
|
+
// ÉTAPE 3b : ÉCHAPPEMENT DES CARACTÈRES (\* \_ \# etc.)
|
|
464
|
+
// Traité après l'extraction du code (le code reste littéral) et avant
|
|
465
|
+
// tout le reste, pour que \* n'ouvre pas une emphase, \# ne crée pas
|
|
466
|
+
// un titre, \- ne crée pas de liste, etc.
|
|
467
|
+
// ====================================================================
|
|
468
|
+
$escapes = [];
|
|
469
|
+
$html = preg_replace_callback(
|
|
470
|
+
'/\\\\([\\\\`*_{}\[\]<>()#+\-.!|])/',
|
|
471
|
+
function ($m) use (&$escapes): string {
|
|
472
|
+
$placeholder = "\x02ESC" . count($escapes) . "\x03";
|
|
473
|
+
$escapes[$placeholder] = htmlspecialchars($m[1], ENT_QUOTES, 'UTF-8');
|
|
474
|
+
return $placeholder;
|
|
475
|
+
},
|
|
476
|
+
$html
|
|
477
|
+
);
|
|
478
|
+
// | est la convention documentée (Markdown Extra / PHP Markdown)
|
|
479
|
+
// pour afficher un pipe littéral dans une cellule de tableau sans
|
|
480
|
+
// qu'il soit interprété comme séparateur de colonnes.
|
|
481
|
+
$html = preg_replace_callback(
|
|
482
|
+
'/|/i',
|
|
483
|
+
function () use (&$escapes): string {
|
|
484
|
+
$placeholder = "\x02ESC" . count($escapes) . "\x03";
|
|
485
|
+
$escapes[$placeholder] = '|';
|
|
486
|
+
return $placeholder;
|
|
487
|
+
},
|
|
488
|
+
$html
|
|
489
|
+
);
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
// ====================================================================
|
|
493
|
+
// ÉTAPE 3d : LIENS AUTOMATIQUES <https://...> et <email@example.com>
|
|
494
|
+
// Traités avant l'encodage XSS car les caractères < > seraient encodés
|
|
495
|
+
// en < > et la regex ne matcherait plus.
|
|
496
|
+
// ====================================================================
|
|
497
|
+
$autolinks = [];
|
|
498
|
+
$html = preg_replace_callback('/<(https?:\/\/[^\s<>]+)>/', function ($m) use (&$autolinks): string {
|
|
499
|
+
$url = htmlspecialchars($m[1], ENT_QUOTES, 'UTF-8');
|
|
500
|
+
$placeholder = "\x02AL" . count($autolinks) . "\x03";
|
|
501
|
+
$autolinks[$placeholder] = "<a href=\"{$url}\" target=\"_blank\" rel=\"noopener noreferrer\">{$url}</a>";
|
|
502
|
+
return $placeholder;
|
|
503
|
+
}, $html);
|
|
504
|
+
$html = preg_replace_callback('/<([^\s<>]+@[^\s<>]+\.[^\s<>]+)>/', function ($m) use (&$autolinks): string {
|
|
505
|
+
$email = htmlspecialchars($m[1], ENT_QUOTES, 'UTF-8');
|
|
506
|
+
$placeholder = "\x02AL" . count($autolinks) . "\x03";
|
|
507
|
+
$autolinks[$placeholder] = "<a href=\"mailto:{$email}\">{$email}</a>";
|
|
508
|
+
return $placeholder;
|
|
509
|
+
}, $html);
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
// ====================================================================
|
|
513
|
+
// ÉTAPE 3e : ALERTES GFM ET BLOCKQUOTES
|
|
164
514
|
// Traités avant l'encodage XSS car le caractère > serait encodé en >
|
|
165
515
|
// et les regex ne matcheraient plus.
|
|
166
516
|
// ====================================================================
|
|
@@ -179,19 +529,26 @@ class MD {
|
|
|
179
529
|
$blockquotes[$placeholder] = "<div class=\"markdown-alert markdown-alert-{$type}\">"
|
|
180
530
|
. "<p class=\"markdown-alert-title\">{$label}</p>"
|
|
181
531
|
. "<p>{$content}</p></div>";
|
|
182
|
-
|
|
532
|
+
// Le \n final consommé par la regex est réinjecté après le
|
|
533
|
+
// placeholder pour ne pas fusionner la ligne vide suivante
|
|
534
|
+
// avec celle du placeholder (ce qui fausserait par exemple
|
|
535
|
+
// la détection d'un titre Setext juste après).
|
|
536
|
+
return $placeholder . (str_ends_with($matches[1], "\n") ? "\n" : '');
|
|
183
537
|
},
|
|
184
538
|
$html
|
|
185
539
|
);
|
|
186
540
|
|
|
187
|
-
// Blockquotes standards
|
|
541
|
+
// Blockquotes standards (imbrication gérée par récursion sur toHtml,
|
|
542
|
+
// qui ré-applique cette même règle sur le contenu déjà dé-préfixé
|
|
543
|
+
// d'un niveau de ">")
|
|
188
544
|
$html = preg_replace_callback('/^((?:>[ \t]?[^\n]*\n?)+)/m', function ($matches) use (&$blockquotes): string {
|
|
189
545
|
$content = preg_replace('/^>[ \t]?/m', '', $matches[1]);
|
|
190
546
|
// Les deux espaces trailing sont laissés tels quels : toHtml() les gère lui-même
|
|
191
547
|
$inner = self::toHtml(trim($content));
|
|
192
548
|
$placeholder = "\x02BQ" . count($blockquotes) . "\x03";
|
|
193
549
|
$blockquotes[$placeholder] = "<blockquote>{$inner}</blockquote>";
|
|
194
|
-
|
|
550
|
+
// Voir commentaire ci-dessus : on préserve le \n final consommé.
|
|
551
|
+
return $placeholder . (str_ends_with($matches[1], "\n") ? "\n" : '');
|
|
195
552
|
}, $html);
|
|
196
553
|
|
|
197
554
|
|
|
@@ -251,7 +608,7 @@ class MD {
|
|
|
251
608
|
|
|
252
609
|
|
|
253
610
|
// ====================================================================
|
|
254
|
-
// ÉTAPE 6 : (Alertes GFM et blockquotes traités à l'étape
|
|
611
|
+
// ÉTAPE 6 : (Alertes GFM et blockquotes traités à l'étape 3e)
|
|
255
612
|
// ====================================================================
|
|
256
613
|
|
|
257
614
|
|
|
@@ -263,44 +620,79 @@ class MD {
|
|
|
263
620
|
|
|
264
621
|
|
|
265
622
|
// ====================================================================
|
|
266
|
-
// ÉTAPE
|
|
623
|
+
// ÉTAPE 7b : TITRES SETEXT (syntaxe alternative == / --)
|
|
624
|
+
// Titre
|
|
625
|
+
// ===== → <h1>
|
|
626
|
+
//
|
|
627
|
+
// Titre
|
|
628
|
+
// ----- → <h2>
|
|
629
|
+
// Traité avant les titres ATX et avant les lignes séparatrices (une
|
|
630
|
+
// ligne de tirets juste après une ligne de texte est un titre, pas un <hr>).
|
|
267
631
|
// ====================================================================
|
|
268
|
-
$html = preg_replace_callback(
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
632
|
+
$html = preg_replace_callback(
|
|
633
|
+
'/^(?![ \t]*(?:#{1,6}[ \t]|>|```|\||[-*+][ \t]|\d+\.[ \t]))[ \t]*(\S.*?)[ \t]*(?:\{#([a-zA-Z0-9_\-:.]+)\}[ \t]*)?\n[ \t]*=+[ \t]*$/m',
|
|
634
|
+
function ($matches) {
|
|
635
|
+
$text = trim($matches[1]);
|
|
636
|
+
$id = !empty($matches[2]) ? $matches[2] : self::slugify($text);
|
|
637
|
+
return "<h1 id=\"{$id}\">{$text}</h1>";
|
|
638
|
+
},
|
|
639
|
+
$html
|
|
640
|
+
);
|
|
641
|
+
$html = preg_replace_callback(
|
|
642
|
+
'/^(?![ \t]*(?:#{1,6}[ \t]|>|```|\||[-*+][ \t]|\d+\.[ \t]))[ \t]*(\S.*?)[ \t]*(?:\{#([a-zA-Z0-9_\-:.]+)\}[ \t]*)?\n[ \t]*-+[ \t]*$/m',
|
|
643
|
+
function ($matches) {
|
|
644
|
+
$text = trim($matches[1]);
|
|
645
|
+
$id = !empty($matches[2]) ? $matches[2] : self::slugify($text);
|
|
646
|
+
return "<h2 id=\"{$id}\">{$text}</h2>";
|
|
647
|
+
},
|
|
648
|
+
$html
|
|
649
|
+
);
|
|
275
650
|
|
|
276
651
|
|
|
277
652
|
// ====================================================================
|
|
278
|
-
// ÉTAPE
|
|
653
|
+
// ÉTAPE 8 : TITRES (ATX : # à ######)
|
|
279
654
|
// ====================================================================
|
|
280
655
|
$html = preg_replace_callback(
|
|
281
|
-
'/^([ \t]
|
|
656
|
+
'/^(#{1,6})[ \t]+(.+?)[ \t]*(?:\{#([a-zA-Z0-9_\-:.]+)\}[ \t]*)?(?:[ \t]+#+)?$/m',
|
|
282
657
|
function ($matches) {
|
|
283
|
-
$
|
|
284
|
-
$
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
$out .= " <li>{$text}</li>\n";
|
|
288
|
-
}
|
|
289
|
-
return $out . "</ol>";
|
|
658
|
+
$level = strlen($matches[1]);
|
|
659
|
+
$text = trim($matches[2]);
|
|
660
|
+
$id = !empty($matches[3]) ? $matches[3] : self::slugify($text);
|
|
661
|
+
return "<h{$level} id=\"{$id}\">{$text}</h{$level}>";
|
|
290
662
|
},
|
|
291
663
|
$html
|
|
292
664
|
);
|
|
293
665
|
|
|
666
|
+
|
|
667
|
+
// ====================================================================
|
|
668
|
+
// ÉTAPE 9 : LISTES (puces et ordonnées, avec imbrication)
|
|
669
|
+
// Une seule passe détecte un bloc contigu de lignes qui sont soit une
|
|
670
|
+
// puce (-,*,+) soit un item numéroté, quel que soit leur niveau
|
|
671
|
+
// d'indentation ; le bloc est ensuite reconstruit récursivement en
|
|
672
|
+
// <ol>/<ul> imbriqués selon la profondeur d'indentation relative.
|
|
673
|
+
// Les items de tâches (déjà convertis en <li class="task-item">) ne
|
|
674
|
+
// matchent plus ce motif et ne sont donc pas ré-englobés ici.
|
|
675
|
+
// ====================================================================
|
|
294
676
|
$html = preg_replace_callback(
|
|
295
|
-
'/^([ \t]*[-*+]
|
|
677
|
+
'/^([ \t]*(?:\d+\.|[-*+])[ \t]+.+(?:\n[ \t]*(?:\d+\.|[-*+])[ \t]+.+)*)/m',
|
|
296
678
|
function ($matches) {
|
|
297
|
-
$
|
|
298
|
-
$
|
|
299
|
-
foreach ($
|
|
300
|
-
|
|
301
|
-
|
|
679
|
+
$lines = explode("\n", $matches[1]);
|
|
680
|
+
$items = [];
|
|
681
|
+
foreach ($lines as $line) {
|
|
682
|
+
if (preg_match('/^([ \t]*)(\d+)\.[ \t]+(.*)$/', $line, $m)) {
|
|
683
|
+
$items[] = ['indent' => self::indentWidth($m[1]), 'type' => 'ol', 'text' => $m[3]];
|
|
684
|
+
} elseif (preg_match('/^([ \t]*)[-*+][ \t]+(.*)$/', $line, $m)) {
|
|
685
|
+
$items[] = ['indent' => self::indentWidth($m[1]), 'type' => 'ul', 'text' => $m[2]];
|
|
686
|
+
}
|
|
302
687
|
}
|
|
303
|
-
|
|
688
|
+
if (empty($items)) return $matches[1];
|
|
689
|
+
// Normalise le niveau d'indentation le plus bas à 0
|
|
690
|
+
$minIndent = min(array_column($items, 'indent'));
|
|
691
|
+
foreach ($items as &$it) $it['indent'] -= $minIndent;
|
|
692
|
+
unset($it);
|
|
693
|
+
|
|
694
|
+
$i = 0;
|
|
695
|
+
return self::buildListTree($items, $i, count($items));
|
|
304
696
|
},
|
|
305
697
|
$html
|
|
306
698
|
);
|
|
@@ -315,7 +707,39 @@ class MD {
|
|
|
315
707
|
|
|
316
708
|
|
|
317
709
|
// ====================================================================
|
|
318
|
-
// ÉTAPE
|
|
710
|
+
// ÉTAPE 9b : LISTES DE DÉFINITION (syntaxe étendue)
|
|
711
|
+
// Terme
|
|
712
|
+
// : Définition
|
|
713
|
+
// ====================================================================
|
|
714
|
+
$html = self::extractDefinitionLists($html);
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
// ====================================================================
|
|
718
|
+
// ÉTAPE 9c : RÉFÉRENCES DE NOTES DE BAS DE PAGE [^label]
|
|
719
|
+
// Converties AVANT l'emphase pour ne pas entrer en collision avec le
|
|
720
|
+
// nouvel exposant ^texte^ (un [^1] suivi plus loin d'un [^2] sur la
|
|
721
|
+
// même ligne pourrait sinon être interprété comme ^1] ... [^2^).
|
|
722
|
+
// La numérotation est séquentielle, dans l'ordre de première
|
|
723
|
+
// apparition dans le texte (comme documenté).
|
|
724
|
+
// ====================================================================
|
|
725
|
+
$footnoteOrder = [];
|
|
726
|
+
$html = preg_replace_callback('/\[\^([^\]\s]+)\]/', function ($m) use (&$footnoteOrder, &$footnoteDefs): string {
|
|
727
|
+
$label = strtolower(trim($m[1]));
|
|
728
|
+
if (!isset($footnoteDefs[$label])) {
|
|
729
|
+
// Référence vers une note non définie : laissée telle quelle.
|
|
730
|
+
return $m[0];
|
|
731
|
+
}
|
|
732
|
+
if (!isset($footnoteOrder[$label])) {
|
|
733
|
+
$footnoteOrder[$label] = count($footnoteOrder) + 1;
|
|
734
|
+
}
|
|
735
|
+
$num = $footnoteOrder[$label];
|
|
736
|
+
return "<sup id=\"fnref:{$label}\"><a href=\"#fn:{$label}\">{$num}</a></sup>";
|
|
737
|
+
}, $html);
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
// ====================================================================
|
|
741
|
+
// ÉTAPE 10 : TEXTE EN LIGNE (Gras, Italique, Barré, Surlignage,
|
|
742
|
+
// Indice/Exposant, Emoji)
|
|
319
743
|
// ====================================================================
|
|
320
744
|
$html = preg_replace('/\*\*\*(.+?)\*\*\*/s', '<strong><em>$1</em></strong>', $html);
|
|
321
745
|
$html = preg_replace('/___(.+?)___/s', '<strong><em>$1</em></strong>', $html);
|
|
@@ -325,7 +749,25 @@ class MD {
|
|
|
325
749
|
// Le _ italique ne doit matcher qu'aux frontières de mots pour ne pas
|
|
326
750
|
// capturer les snake_case, noms de packages (@php-wasm/node), etc.
|
|
327
751
|
$html = preg_replace('/(?<!\w)_([^_\n]+)_(?!\w)/', '<em>$1</em>', $html);
|
|
752
|
+
// Surlignage ==texte== (syntaxe étendue)
|
|
753
|
+
$html = preg_replace('/==(.+?)==/s', '<mark>$1</mark>', $html);
|
|
754
|
+
// Barré ~~texte~~ — traité AVANT le sous-script (simple ~) pour que
|
|
755
|
+
// celui-ci ne matche pas la moitié d'une paire de tildes doubles.
|
|
328
756
|
$html = preg_replace('/~~(.+?)~~/s', '<del>$1</del>', $html);
|
|
757
|
+
// Exposant ^texte^ (syntaxe étendue) — placé avant l'échappement des
|
|
758
|
+
// références de notes ([^label]) n'est pas un souci : celles-ci sont
|
|
759
|
+
// encadrées de crochets et ne forment donc pas de paire ^...^ isolée.
|
|
760
|
+
$html = preg_replace('/\^([^\^\n]+)\^/', '<sup>$1</sup>', $html);
|
|
761
|
+
// Sous-script ~texte~ (un seul tilde ; les ~~ ont déjà été consommés
|
|
762
|
+
// juste au-dessus par le barré).
|
|
763
|
+
$html = preg_replace('/~([^~\n]+)~/', '<sub>$1</sub>', $html);
|
|
764
|
+
|
|
765
|
+
// Émojis :shortcode: (syntaxe étendue) — les raccourcis inconnus sont
|
|
766
|
+
// laissés tels quels plutôt que silencieusement supprimés.
|
|
767
|
+
$html = preg_replace_callback('/:([a-zA-Z0-9_+\-]+):/', function ($m): string {
|
|
768
|
+
$emoji = self::emojiFor($m[1]);
|
|
769
|
+
return $emoji ?? $m[0];
|
|
770
|
+
}, $html);
|
|
329
771
|
|
|
330
772
|
|
|
331
773
|
// ====================================================================
|
|
@@ -339,29 +781,39 @@ class MD {
|
|
|
339
781
|
$html
|
|
340
782
|
);
|
|
341
783
|
|
|
342
|
-
|
|
784
|
+
$buildLink = static function (string $text, string $href, string $title): string {
|
|
785
|
+
$titleAttr = $title !== '' ? ' title="' . $title . '"' : '';
|
|
786
|
+
$extern = preg_match('/^https?:\/\//i', $href)
|
|
787
|
+
? ' target="_blank" rel="noopener noreferrer"'
|
|
788
|
+
: '';
|
|
789
|
+
return "<a href=\"{$href}\"{$titleAttr}{$extern}>{$text}</a>";
|
|
790
|
+
};
|
|
791
|
+
|
|
792
|
+
// Liens par référence [texte][label] et [texte][] (raccourci = label = texte)
|
|
343
793
|
$html = preg_replace_callback(
|
|
344
|
-
'/\[([^\]]+)\]\
|
|
345
|
-
function ($m): string {
|
|
346
|
-
$text
|
|
347
|
-
$
|
|
348
|
-
|
|
349
|
-
$
|
|
350
|
-
|
|
351
|
-
: '';
|
|
352
|
-
return "<a href=\"{$href}\"{$title}{$extern}>{$text}</a>";
|
|
794
|
+
'/\[([^\]]+)\]\[([^\]]*)\]/',
|
|
795
|
+
function ($m) use (&$refDefs, $buildLink): string {
|
|
796
|
+
$text = $m[1];
|
|
797
|
+
$label = strtolower(trim($m[2] !== '' ? $m[2] : $m[1]));
|
|
798
|
+
if (!isset($refDefs[$label])) return $m[0];
|
|
799
|
+
$def = $refDefs[$label];
|
|
800
|
+
return $buildLink($text, $def['url'], $def['title']);
|
|
353
801
|
},
|
|
354
802
|
$html
|
|
355
803
|
);
|
|
356
804
|
|
|
357
|
-
// Liens
|
|
358
|
-
$html =
|
|
359
|
-
'
|
|
360
|
-
|
|
805
|
+
// Liens markdown [texte](url "titre optionnel")
|
|
806
|
+
$html = preg_replace_callback(
|
|
807
|
+
'/\[([^\]]+)\]\(([^)\s]+)(?:\s+"([^"]*)")?\)/',
|
|
808
|
+
function ($m) use ($buildLink): string {
|
|
809
|
+
return $buildLink($m[1], $m[2], $m[3] ?? '');
|
|
810
|
+
},
|
|
361
811
|
$html
|
|
362
812
|
);
|
|
363
813
|
|
|
364
|
-
// URL nues https://...
|
|
814
|
+
// URL nues https://... (syntaxe étendue : auto-link sans crochets).
|
|
815
|
+
// Exclut celles déjà entre guillemets/attributs (href="...") ou déjà
|
|
816
|
+
// transformées en lien pour ne pas les doubler.
|
|
365
817
|
$html = preg_replace(
|
|
366
818
|
'/(?<!["\'=>])\b(https?:\/\/[^\s<>"\')\]]+)/',
|
|
367
819
|
'<a href="$1" target="_blank" rel="noopener noreferrer">$1</a>',
|
|
@@ -384,12 +836,16 @@ class MD {
|
|
|
384
836
|
// ====================================================================
|
|
385
837
|
$blockStartTags = ['<h', '<pre', '<ul', '<ol', '<li', '<table', '<thead', '<tbody',
|
|
386
838
|
'<tr', '<td', '<th', '<blockquote', '<div', '<hr', '<img',
|
|
387
|
-
'
|
|
388
|
-
"\x02CB", "\
|
|
839
|
+
'<dl', '<dt', '<dd',
|
|
840
|
+
"\x02CB", "\x02PLG", "\x02BQ"];
|
|
389
841
|
|
|
390
842
|
$isBlockLine = static function (string $line) use ($blockStartTags): bool {
|
|
391
843
|
$t = ltrim($line);
|
|
392
844
|
if ($t === '') return false;
|
|
845
|
+
// Toute balise fermante (</...>) est toujours considérée comme une
|
|
846
|
+
// ligne "bloc" : ça évite qu'une fermeture de <table>, <thead>,
|
|
847
|
+
// <tr>, etc. finisse absorbée dans un <p> environnant.
|
|
848
|
+
if (str_starts_with($t, '</')) return true;
|
|
393
849
|
foreach ($blockStartTags as $tag) {
|
|
394
850
|
if (str_starts_with($t, $tag)) return true;
|
|
395
851
|
}
|
|
@@ -437,8 +893,27 @@ class MD {
|
|
|
437
893
|
$html = strtr($html, $blockquotes);
|
|
438
894
|
$html = strtr($html, $codeBlocks);
|
|
439
895
|
$html = strtr($html, $inlineCodes);
|
|
896
|
+
$html = strtr($html, $autolinks);
|
|
897
|
+
// Les échappements sont réinjectés en tout dernier, une fois que plus
|
|
898
|
+
// aucune regex Markdown ne peut les interpréter.
|
|
899
|
+
$html = strtr($html, $escapes);
|
|
900
|
+
|
|
901
|
+
|
|
902
|
+
// ====================================================================
|
|
903
|
+
// ÉTAPE 15 : BLOC DES NOTES DE BAS DE PAGE
|
|
904
|
+
// Ajouté en fin de document, uniquement si au moins une note a été
|
|
905
|
+
// référencée (les notes définies mais jamais référencées sont
|
|
906
|
+
// silencieusement ignorées).
|
|
907
|
+
// ====================================================================
|
|
908
|
+
if (!empty($footnoteOrder)) {
|
|
909
|
+
$html .= "\n<div class=\"footnotes\">\n<ol>\n";
|
|
910
|
+
foreach ($footnoteOrder as $label => $num) {
|
|
911
|
+
$content = $footnoteDefs[$label];
|
|
912
|
+
$html .= " <li id=\"fn:{$label}\">{$content} <a href=\"#fnref:{$label}\" class=\"footnote-backref\">↩</a></li>\n";
|
|
913
|
+
}
|
|
914
|
+
$html .= "</ol>\n</div>";
|
|
915
|
+
}
|
|
440
916
|
|
|
441
917
|
return $html;
|
|
442
918
|
}
|
|
443
|
-
}
|
|
444
|
-
|
|
919
|
+
}
|
|
@@ -54,6 +54,137 @@ class YAML
|
|
|
54
54
|
return self::parse(file_get_contents($path), $assoc);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Charge un fichier YAML ou JSON, puis parcourt récursivement le résultat
|
|
59
|
+
* et remplace toute valeur string qui correspond à un chemin relatif vers
|
|
60
|
+
* un fichier YAML/JSON existant par le contenu désérialisé de ce fichier.
|
|
61
|
+
*
|
|
62
|
+
* Chaque fichier inclus est lui-même résolu relativement à son propre
|
|
63
|
+
* répertoire, et ainsi de suite (récursif).
|
|
64
|
+
*
|
|
65
|
+
* Si la chaîne ne se termine pas par .yml/.yaml/.json, ou si le fichier
|
|
66
|
+
* résolu n'existe pas, la valeur est conservée telle quelle.
|
|
67
|
+
*
|
|
68
|
+
* Les références circulaires (ex. A → B → A) lèvent une RuntimeException.
|
|
69
|
+
*
|
|
70
|
+
* Utilisation :
|
|
71
|
+
* $data = YAML::loadFile('/chemin/vers/config.yaml');
|
|
72
|
+
* $data = YAML::loadFile('/chemin/vers/config.yaml', true); // arrays assoc
|
|
73
|
+
*
|
|
74
|
+
* @param string $path Chemin vers le fichier racine (YAML ou JSON).
|
|
75
|
+
* @param bool $assoc true → mappings en array, false → stdClass.
|
|
76
|
+
* @return mixed
|
|
77
|
+
*/
|
|
78
|
+
public static function loadFile(string $path, bool $assoc = false): mixed
|
|
79
|
+
{
|
|
80
|
+
$absolute = realpath($path);
|
|
81
|
+
if ($absolute === false || !is_readable($absolute)) {
|
|
82
|
+
throw new \RuntimeException("Impossible de lire le fichier : $path");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return self::loadFileRecursive($absolute, $assoc, []);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// -------------------------------------------------------------------------
|
|
89
|
+
// Méthodes privées pour loadFile
|
|
90
|
+
// -------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Charge et résout un fichier, en propageant la liste des ancêtres pour
|
|
94
|
+
* détecter les cycles.
|
|
95
|
+
*
|
|
96
|
+
* @param string $absolute Chemin absolu canonique du fichier à charger.
|
|
97
|
+
* @param bool $assoc
|
|
98
|
+
* @param string[] $ancestors Chemins absolus des fichiers en cours de traitement.
|
|
99
|
+
* @return mixed
|
|
100
|
+
*/
|
|
101
|
+
private static function loadFileRecursive(string $absolute, bool $assoc, array $ancestors): mixed
|
|
102
|
+
{
|
|
103
|
+
if (in_array($absolute, $ancestors, true)) {
|
|
104
|
+
throw new \RuntimeException(
|
|
105
|
+
"Référence circulaire détectée : " . implode(' → ', $ancestors) . " → $absolute"
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
$ext = strtolower(pathinfo($absolute, PATHINFO_EXTENSION));
|
|
110
|
+
$raw = file_get_contents($absolute);
|
|
111
|
+
$dir = dirname($absolute);
|
|
112
|
+
|
|
113
|
+
if ($ext === 'json') {
|
|
114
|
+
$data = json_decode($raw, $assoc, 512, JSON_THROW_ON_ERROR);
|
|
115
|
+
} else {
|
|
116
|
+
// .yml, .yaml ou autre extension traitée comme YAML
|
|
117
|
+
$data = self::parse($raw, $assoc);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return self::resolveNode($data, $dir, $assoc, [...$ancestors, $absolute]);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Parcourt récursivement une valeur PHP (objet stdClass, array, string,
|
|
125
|
+
* scalaire) et résout les références vers des fichiers externes.
|
|
126
|
+
*
|
|
127
|
+
* @param mixed $node
|
|
128
|
+
* @param string $dir Répertoire du fichier qui contient ce nœud.
|
|
129
|
+
* @param bool $assoc
|
|
130
|
+
* @param string[] $ancestors
|
|
131
|
+
* @return mixed
|
|
132
|
+
*/
|
|
133
|
+
private static function resolveNode(mixed $node, string $dir, bool $assoc, array $ancestors): mixed
|
|
134
|
+
{
|
|
135
|
+
if (is_string($node)) {
|
|
136
|
+
return self::resolveString($node, $dir, $assoc, $ancestors);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (is_array($node)) {
|
|
140
|
+
foreach ($node as $key => $value) {
|
|
141
|
+
$node[$key] = self::resolveNode($value, $dir, $assoc, $ancestors);
|
|
142
|
+
}
|
|
143
|
+
return $node;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if ($node instanceof \stdClass) {
|
|
147
|
+
foreach ($node as $key => $value) {
|
|
148
|
+
$node->$key = self::resolveNode($value, $dir, $assoc, $ancestors);
|
|
149
|
+
}
|
|
150
|
+
return $node;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// int, float, bool, null → retourné tel quel
|
|
154
|
+
return $node;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Si la chaîne pointe vers un fichier YAML/JSON existant (chemin relatif
|
|
159
|
+
* au répertoire $dir), charge ce fichier récursivement. Sinon retourne la
|
|
160
|
+
* chaîne d'origine.
|
|
161
|
+
*
|
|
162
|
+
* @param string $str
|
|
163
|
+
* @param string $dir
|
|
164
|
+
* @param bool $assoc
|
|
165
|
+
* @param string[] $ancestors
|
|
166
|
+
* @return mixed
|
|
167
|
+
*/
|
|
168
|
+
private static function resolveString(string $str, string $dir, bool $assoc, array $ancestors): mixed
|
|
169
|
+
{
|
|
170
|
+
$trimmed = trim($str);
|
|
171
|
+
|
|
172
|
+
// Filtre rapide sur l'extension
|
|
173
|
+
if (!preg_match('/\.(ya?ml|json)$/i', $trimmed)) {
|
|
174
|
+
return $str;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Résolution du chemin relatif au répertoire du fichier parent
|
|
178
|
+
$candidate = $dir . DIRECTORY_SEPARATOR . $trimmed;
|
|
179
|
+
$absolute = realpath($candidate);
|
|
180
|
+
|
|
181
|
+
if ($absolute === false || !is_readable($absolute)) {
|
|
182
|
+
return $str; // Fichier introuvable → string ordinaire
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return self::loadFileRecursive($absolute, $assoc, $ancestors);
|
|
186
|
+
}
|
|
187
|
+
|
|
57
188
|
// -------------------------------------------------------------------------
|
|
58
189
|
// Parsing récursif
|
|
59
190
|
// -------------------------------------------------------------------------
|
package/src/prepros.js
CHANGED
|
@@ -1,16 +1,48 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path, { dirname } from "path";
|
|
3
|
+
import isBinary from './utils/isbinary.js';
|
|
4
|
+
import joinWith from './utils/joinwith.js'
|
|
3
5
|
import { spawn } from 'child_process';
|
|
4
6
|
import { fileURLToPath } from 'url';
|
|
7
|
+
import { walkFile } from '@kirigami/struct-walker';
|
|
5
8
|
import { getPHPRuntime, getPHPRuntimeWithNetwork } from "@kirigami/php-wasm";
|
|
6
|
-
import isBinary from './utils/isbinary.js';
|
|
7
|
-
import joinWith from './utils/joinwith.js'
|
|
8
|
-
import yaml from "js-yaml";
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
const __project = process.cwd();
|
|
12
12
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
13
13
|
const __configpath = path.join(__project, 'kirigami.yaml');
|
|
14
|
+
let __root = null;
|
|
15
|
+
let __php = null;
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
if (!fs.existsSync(__configpath)) throw `Config file not found: ${__configpath}`;
|
|
19
|
+
const config = await walkFile(__configpath);
|
|
20
|
+
if(!config) throw `Invalid config file: ${__configpath}`;
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
const getPHPInstance = async () => {
|
|
24
|
+
if(!__php) {
|
|
25
|
+
if(config?.kirigami?.root === undefined) throw `Missing prepros:root property in config file: ${__configpath}`;
|
|
26
|
+
__root = path.join(__project, config.kirigami.root);
|
|
27
|
+
if (!fs.existsSync(__root)) throw `Invalid prepros:root path: ${__root}`;
|
|
28
|
+
|
|
29
|
+
const preprosConfig = config.prepros;
|
|
30
|
+
preprosConfig.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
31
|
+
preprosConfig.root = joinWith('/project/', config?.kirigami?.root);
|
|
32
|
+
preprosConfig.data = config.kirigami || {};
|
|
33
|
+
|
|
34
|
+
__php = await (preprosConfig.network ? getPHPRuntimeWithNetwork() : getPHPRuntime());
|
|
35
|
+
__php.setSpawnHandler((command, args, options) => spawn(command, args, options));
|
|
36
|
+
__php.preprosConfig = preprosConfig;
|
|
37
|
+
|
|
38
|
+
const mountPaths = [];
|
|
39
|
+
const __cache = path.join(__project, '.cache.db');
|
|
40
|
+
mountPath(__php, __dirname, '/prepros');
|
|
41
|
+
mountPath(__php, joinWith(__project, config?.kirigami?.root), joinWith('/project', config?.kirigami?.root));
|
|
42
|
+
if (fs.existsSync(__cache)) mountPath(__php, __cache, '/project/.cache.db');
|
|
43
|
+
}
|
|
44
|
+
return __php;
|
|
45
|
+
}
|
|
14
46
|
|
|
15
47
|
|
|
16
48
|
const mountPath = (php, localPath, virtualDir) => {
|
|
@@ -32,14 +64,16 @@ const mountPath = (php, localPath, virtualDir) => {
|
|
|
32
64
|
|
|
33
65
|
|
|
34
66
|
const run = async (args) => {
|
|
35
|
-
php
|
|
67
|
+
const php = await getPHPInstance();
|
|
36
68
|
const output = await php.runStream({
|
|
37
69
|
scriptPath: '/prepros/prepros.php',
|
|
38
|
-
env: {
|
|
70
|
+
env: {
|
|
71
|
+
PREPROS_ARGS: JSON.stringify(args),
|
|
72
|
+
PREPROS_CONFIG: JSON.stringify(php.preprosConfig)
|
|
73
|
+
}
|
|
39
74
|
});
|
|
40
75
|
const stdout = await output.stdoutText;
|
|
41
76
|
const stderr = await output.stderrText;
|
|
42
|
-
// console.log(stdout);
|
|
43
77
|
let retobj;
|
|
44
78
|
try {
|
|
45
79
|
retobj = JSON.parse(stdout);
|
|
@@ -64,44 +98,29 @@ const run = async (args) => {
|
|
|
64
98
|
|
|
65
99
|
|
|
66
100
|
const render = async (file = '.') => {
|
|
101
|
+
const php = await getPHPInstance();
|
|
67
102
|
const target = path.resolve(__project, config?.kirigami?.root, file);
|
|
68
103
|
const fsvm = path.join('/project', config?.kirigami?.root, file).replace(/\\/g, '/');
|
|
69
104
|
mountPath(php, target, fsvm);
|
|
105
|
+
if(config?.prepros?.before) {
|
|
106
|
+
const include = path.resolve(__project, config?.kirigami?.root, config?.prepros?.before);
|
|
107
|
+
const dest = path.join('/project', config?.kirigami?.root, config?.prepros?.before).replace(/\\/g, '/');
|
|
108
|
+
mountPath(php, include, dest);
|
|
109
|
+
}
|
|
110
|
+
if(config?.prepros?.after) {
|
|
111
|
+
const include = path.resolve(__project, config?.kirigami?.root, config?.prepros?.after);
|
|
112
|
+
const dest = path.join('/project', config?.kirigami?.root, config?.prepros?.after).replace(/\\/g, '/');
|
|
113
|
+
mountPath(php, include, dest);
|
|
114
|
+
}
|
|
70
115
|
return run([fsvm]);
|
|
71
116
|
}
|
|
72
117
|
|
|
73
118
|
|
|
74
119
|
const sitemap = async (dir) => {
|
|
120
|
+
const php = await getPHPInstance();
|
|
75
121
|
mountPath(php, __root, '/project/' + config?.kirigami?.root);
|
|
76
122
|
return run(['sitemap']);
|
|
77
123
|
}
|
|
78
124
|
|
|
79
125
|
|
|
80
|
-
if (!fs.existsSync(__configpath)) throw `Config file not found: ${__configpath}`;
|
|
81
|
-
const configContents = fs.readFileSync(__configpath, 'utf8');
|
|
82
|
-
const config = yaml.load(configContents);
|
|
83
|
-
if(!config) throw `Invalid config file: ${__configpath}`;
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
if(config?.kirigami?.root === undefined) throw `Missing prepros:root property in config file: ${__configpath}`;
|
|
87
|
-
const __root = path.join(__project, config.kirigami.root);
|
|
88
|
-
if (!fs.existsSync(__root)) throw `Invalid prepros:root path: ${__root}`;
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const preprosConfig = config.prepros;
|
|
92
|
-
preprosConfig.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
93
|
-
preprosConfig.root = joinWith('/project/', config?.kirigami?.root);
|
|
94
|
-
preprosConfig.data = config.kirigami || {};
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const php = await (preprosConfig.network ? getPHPRuntimeWithNetwork() : getPHPRuntime());
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const mountPaths = [];
|
|
101
|
-
const __cache = path.join(__project, '.cache.db');
|
|
102
|
-
mountPath(php, __dirname, '/prepros');
|
|
103
|
-
mountPath(php, joinWith(__project, config?.kirigami?.root), joinWith('/project', config?.kirigami?.root));
|
|
104
|
-
if (fs.existsSync(__cache)) mountPath(php, __cache, '/project/.cache.db');
|
|
105
|
-
|
|
106
|
-
|
|
107
126
|
export { render, sitemap };
|
package/src/prepros.php
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
<?php
|
|
2
2
|
|
|
3
|
-
ini_set('display_errors', 1);
|
|
4
3
|
ini_set('log_errors', 1);
|
|
5
|
-
ini_set('error_log', 'php://stderr');
|
|
6
4
|
ini_set('html_errors', 0);
|
|
5
|
+
ini_set('display_errors', 1);
|
|
7
6
|
ini_set('error_reporting', 32767);
|
|
7
|
+
ini_set('error_log', 'php://stderr');
|
|
8
8
|
error_reporting(E_ALL);
|
|
9
9
|
|
|
10
10
|
|