@kirigami/php-prepros 1.9.3 → 3.0.0

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.
@@ -1,6 +1,16 @@
1
1
  <?php
2
2
 
3
- class Normalizer
3
+ /**
4
+ * NORMALIZER_LEGACY — pure-PHP Unicode normalizer (a port of
5
+ * symfony/polyfill-intl-normalizer), with the standard Normalizer API.
6
+ *
7
+ * Retired: ext-intl isn't in the WASM build, but the native `norm` extension
8
+ * (php-kirigami/php-norm, utf8proc, statically built into @kirigami/php-wasm)
9
+ * now provides the real `Normalizer` class and `normalizer_*()` functions.
10
+ * Kept here as a reference implementation and an easy rollback path;
11
+ * autoloadable, but not used anywhere by default.
12
+ */
13
+ class NORMALIZER_LEGACY
4
14
  {
5
15
  const NONE = 2;
6
16
  const FORM_D = 4;
@@ -15,6 +15,8 @@ final class PREPROS
15
15
  public static function loadConfig(object $config)
16
16
  {
17
17
  self::$config = $config;
18
+ self::$config->before ??= null;
19
+ self::$config->after ??= null;
18
20
  self::$root = realpath($config->root) . S;
19
21
  if (!empty($config->includes)) foreach ($config->includes as $path) {
20
22
  if (!is_file(realpath(self::$root . $path))) continue;
@@ -28,6 +30,25 @@ final class PREPROS
28
30
  }
29
31
 
30
32
 
33
+ // Only page files beneath the configured source root are publishable.
34
+ // Check every relative directory, but not the configured root's own name.
35
+ public static function isPage(string $file): bool
36
+ {
37
+ $file = realpath($file);
38
+ if (!$file || !is_file($file)) return false;
39
+ $file = str_replace('\\', '/', $file);
40
+ $root = str_replace('\\', '/', self::$root);
41
+ if (!str_starts_with($file, $root)) return false;
42
+ $parts = explode('/', substr($file, strlen($root)));
43
+ $name = array_pop($parts);
44
+ if (!preg_match('/^_.*\.php$/i', $name)) return false;
45
+ foreach ($parts as $part) {
46
+ if (str_starts_with($part, '_')) return false;
47
+ }
48
+ return true;
49
+ }
50
+
51
+
31
52
  public static function render(string $file)
32
53
  {
33
54
  $dir = pathinfo($file, PATHINFO_DIRNAME) . S;
@@ -67,6 +88,20 @@ final class PREPROS
67
88
  }, explode(PHP_EOL, $body)));
68
89
  }
69
90
 
91
+ $typeConfig = !empty($type) ? (self::$config->types->{$type} ?? null) : null;
92
+
93
+ ob_start();
94
+ self::processHook('pre_type_before', $typeConfig->before ?? null);
95
+ if (!empty($typeConfig->before)) include(realpath(self::$root . $typeConfig->before));
96
+ $typeHeader = self::processHook('post_type_before', ob_get_clean());
97
+
98
+ ob_start();
99
+ self::processHook('pre_type_after', $typeConfig->after ?? null);
100
+ if (!empty($typeConfig->after)) include(realpath(self::$root . $typeConfig->after));
101
+ $typeFooter = self::processHook('post_type_after', ob_get_clean());
102
+
103
+ $body = $typeHeader . $body . $typeFooter;
104
+
70
105
  ob_start();
71
106
  self::processHook('pre_after', self::$config->after);
72
107
  if (self::$config->after) include(realpath(self::$root . self::$config->after));
@@ -96,9 +131,7 @@ final class PREPROS
96
131
  $paths = [];
97
132
  $root = realpath(self::$root);
98
133
  foreach (FS::dig($root . '/_index.php') as $file) {
99
- $parent = pathinfo(pathinfo($file, PATHINFO_DIRNAME), PATHINFO_BASENAME);
100
- if (strpos($parent, '_') === 0) continue;
101
- if (strpos(pathinfo($file, PATHINFO_FILENAME), '_') !== 0) continue;
134
+ if (!self::isPage($file)) continue;
102
135
  $paths[] = str_replace('\\', '/', ltrim(str_replace($root, '', pathinfo(realpath($file), PATHINFO_DIRNAME)), DIRECTORY_SEPARATOR));
103
136
  }
104
137
  if (empty($paths)) $paths[] = '';
@@ -127,12 +160,40 @@ final class PREPROS
127
160
  $destrobots = self::$root . 'robots.txt';
128
161
  $urlrobots = rtrim(self::$config->data->baseurl, '/') . '/sitemap.xml';
129
162
  file_put_contents($destrobots, "User-agent: *\nAllow: /\nSitemap: {$urlrobots}");
130
-
131
- self::exportFile([$dest, $destrobots]);
163
+
164
+ $desthumans = self::humans();
165
+
166
+ self::exportFile(array_filter([$dest, $destrobots, $desthumans]));
132
167
  return realpath($dest);
133
168
  }
134
169
 
135
170
 
171
+ // Generates humans.txt (humanstxt.org) from the kirigami: block's
172
+ // author/email — same source fields config.js's fillBanner() already
173
+ // pulls ###AUTHOR###/###EMAIL### from, same spirit: fill in what the
174
+ // project already declared instead of asking for it twice. Skipped
175
+ // entirely when neither is set — nothing meaningful to write.
176
+ private static function humans(): ?string
177
+ {
178
+ $author = trim((string) (self::$config->data->author ?? ''));
179
+ $email = trim((string) (self::$config->data->email ?? ''));
180
+ if ($author === '' && $email === '') return null;
181
+
182
+ $lines = ['/* TEAM */', ''];
183
+ if ($author !== '') $lines[] = " {$author}";
184
+ if ($email !== '') $lines[] = " Contact: {$email}";
185
+ $lines[] = '';
186
+ $lines[] = '/* SITE */';
187
+ $lines[] = '';
188
+ $lines[] = ' Last update: ' . date('Y-m-d');
189
+ $lines[] = ' Software: Kirigami -- https://php-kirigami.github.io';
190
+
191
+ $dest = self::$root . 'humans.txt';
192
+ file_put_contents($dest, implode("\n", $lines) . "\n");
193
+ return $dest;
194
+ }
195
+
196
+
136
197
  /**
137
198
  * Auto-wires each page: a theme/FOUC guard as the first child of <head>, a
138
199
  * <link> for every `sass` task output, a <script> (no `defer`, before
@@ -146,6 +207,22 @@ final class PREPROS
146
207
  * only expanded on export (see replaceTokens()), so rebuilding a preview
147
208
  * never rewrites the committed page.
148
209
  */
210
+ // Whether $contents already has a real <$tag … $attr="…$needle…"> —
211
+ // not just a mention of the filename somewhere in the page (prose, a
212
+ // <code> block documenting real build output, an HTML comment). The
213
+ // naive str_contains() this replaces matched any occurrence anywhere,
214
+ // so a page that merely *documents* the compiled filename (e.g. a
215
+ // docs page showing captured `kiri build` output) got its real
216
+ // <link>/<script> injection skipped — found in production on
217
+ // php-kirigami.github.io's own docs/cli page.
218
+ private static function hasAssetTag(string $contents, string $tag, string $attr, string $needle): bool
219
+ {
220
+ $pattern = '/<' . $tag . '\b[^>]*\b' . $attr . '\s*=\s*["\'][^"\']*'
221
+ . preg_quote($needle, '/') . '[^"\']*["\'][^>]*>/i';
222
+ return (bool) preg_match($pattern, $contents);
223
+ }
224
+
225
+
149
226
  private static function injectHead(string $contents, string $relroot): string
150
227
  {
151
228
  $inject = [];
@@ -180,12 +257,12 @@ final class PREPROS
180
257
 
181
258
  if (($task['type'] ?? '') === 'sass') {
182
259
  $out = preg_replace('/\.s?css$/', '.min.css', $entry);
183
- if ($out !== $entry && !str_contains($contents, $out)) {
260
+ if ($out !== $entry && !self::hasAssetTag($contents, 'link', 'href', $out)) {
184
261
  $links[] = '<link rel="stylesheet" href="' . $relroot . $out . '?###TIMESTAMP###">';
185
262
  }
186
263
  } elseif (($task['type'] ?? '') === 'esbuild') {
187
264
  $out = preg_replace('/\.[jt]s$/', '.min.js', $entry);
188
- if ($out !== $entry && !str_contains($contents, $out)) {
265
+ if ($out !== $entry && !self::hasAssetTag($contents, 'script', 'src', $out)) {
189
266
  $scripts[] = '<script src="' . $relroot . $out . '?###TIMESTAMP###"></script>';
190
267
  }
191
268
  }
@@ -325,4 +402,4 @@ final class PREPROS
325
402
  }
326
403
 
327
404
 
328
- include_once(__DIR__ . '/prepros.plugins.php');
405
+ include_once(__DIR__ . '/prepros.plugins.php');