@orderlyshop/web-components 0.1.0-build.7045
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/AGENTS.md +110 -0
- package/README.md +685 -0
- package/bin/orderly-build-category-pages.mjs +160 -0
- package/bin/orderly-generate-category-pages.mjs +308 -0
- package/bin/orderly-hydrate-static-pages.mjs +595 -0
- package/bin/orderly-init-navigation.mjs +327 -0
- package/bin/orderly-init-shop.mjs +876 -0
- package/bin/orderly-init-taxonomy.mjs +2 -0
- package/bin/orderly-publish-site.mjs +342 -0
- package/custom-elements.json +505 -0
- package/dist/browser/orderly-web-components.define.global.js +3551 -0
- package/dist/browser/orderly-web-components.define.global.js.map +1 -0
- package/dist/browser/orderly-web-components.global.js +3551 -0
- package/dist/browser/orderly-web-components.global.js.map +1 -0
- package/dist/default-shop-DgX6uy10.d.ts +221 -0
- package/dist/default-shop.d.ts +6 -0
- package/dist/default-shop.js +762 -0
- package/dist/default-shop.js.map +1 -0
- package/dist/define-BNMhl19n.d.ts +9 -0
- package/dist/define.d.ts +2 -0
- package/dist/define.js +11094 -0
- package/dist/define.js.map +1 -0
- package/dist/index.d.ts +683 -0
- package/dist/index.js +11417 -0
- package/dist/index.js.map +1 -0
- package/dist/navigation.d.ts +61 -0
- package/dist/navigation.js +1125 -0
- package/dist/navigation.js.map +1 -0
- package/dist/query.d.ts +31 -0
- package/dist/query.js +115 -0
- package/dist/query.js.map +1 -0
- package/dist/registry-CPDecU3g.d.ts +6 -0
- package/dist/shop-BgQhGRzS.d.ts +173 -0
- package/dist/shop-query.d.ts +8 -0
- package/dist/shop-query.js +100 -0
- package/dist/shop-query.js.map +1 -0
- package/dist/shop.d.ts +8 -0
- package/dist/shop.js +11187 -0
- package/dist/shop.js.map +1 -0
- package/dist/stores.d.ts +46 -0
- package/dist/stores.js +145 -0
- package/dist/stores.js.map +1 -0
- package/dist/taxonomy.d.ts +35 -0
- package/dist/taxonomy.js +247 -0
- package/dist/taxonomy.js.map +1 -0
- package/dist/types-Bjez59Hr.d.ts +96 -0
- package/docs/components/README.md +708 -0
- package/docs/components/product-grid.md +182 -0
- package/docs/components/product-rail.md +174 -0
- package/examples/shop/README.md +72 -0
- package/examples/shop/package.json +28 -0
- package/examples/shop/src/category.html +20 -0
- package/examples/shop/src/checkout.html +21 -0
- package/examples/shop/src/forretningsbetingelser.html +81 -0
- package/examples/shop/src/includes/body-end.html +1 -0
- package/examples/shop/src/includes/body-start.html +3 -0
- package/examples/shop/src/includes/head.html +32 -0
- package/examples/shop/src/index.html +25 -0
- package/examples/shop/src/navigation.ts +154 -0
- package/examples/shop/src/product.html +24 -0
- package/examples/shop/src/templates/page-layouts.html +162 -0
- package/examples/shop/src/templates/shop-footer.html +76 -0
- package/examples/shop/tsconfig.json +32 -0
- package/examples/shop/vite.config.mjs +190 -0
- package/html-custom-data.json +279 -0
- package/package.json +118 -0
- package/server/README.md +111 -0
- package/server/apache/.htaccess +18 -0
- package/server/nginx/orderly-products.conf +24 -0
- package/server/node/product-snapshot-server.mjs +133 -0
- package/server/php/orderly-product.php +204 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createServer } from "node:http";
|
|
3
|
+
import { create } from "@bufbuild/protobuf";
|
|
4
|
+
import { anyUnpack } from "@bufbuild/protobuf/wkt";
|
|
5
|
+
import {
|
|
6
|
+
SearchObjectSchema,
|
|
7
|
+
SearchQuerySchema
|
|
8
|
+
} from "@orderlyshop/core-client";
|
|
9
|
+
import { createOrderlyNodeClient } from "@orderlyshop/core-client/node";
|
|
10
|
+
|
|
11
|
+
const ORDERLY_SHARE_URL_PREFIX = "https://orderly.shop/";
|
|
12
|
+
const DEFAULT_IMAGE_BASE_URL = "https://orderlyproduction.azureedge.net/";
|
|
13
|
+
|
|
14
|
+
const port = Number.parseInt(process.env.PORT ?? process.env.ORDERLY_PRODUCT_SNAPSHOT_PORT ?? "4107", 10);
|
|
15
|
+
const baseUrl = process.env.ORDERLY_BACKEND_URL ?? process.env.VITE_ORDERLY_BASE_URL ?? "https://service.orderly.shop";
|
|
16
|
+
const protocol = process.env.ORDERLY_BACKEND_PROTOCOL === "grpc" ? "grpc" : "grpc-web";
|
|
17
|
+
const imageBaseUrl = process.env.ORDERLY_IMAGE_BASE_URL ?? process.env.VITE_ORDERLY_IMAGE_BASE_URL ?? DEFAULT_IMAGE_BASE_URL;
|
|
18
|
+
const client = createOrderlyNodeClient({ baseUrl, protocol });
|
|
19
|
+
|
|
20
|
+
const server = createServer(async (request, response) => {
|
|
21
|
+
const url = new URL(request.url ?? "/", `http://${request.headers.host ?? "localhost"}`);
|
|
22
|
+
if (url.pathname !== "/orderly-product-snapshot") {
|
|
23
|
+
writeJson(response, 404, { error: "Not found" });
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const shareUrl = normalizeShareUrl(url.searchParams.get("url") ?? "");
|
|
28
|
+
if (!shareUrl) {
|
|
29
|
+
writeJson(response, 400, { error: "Missing url query parameter" });
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const result = await client.services.searchService.search(create(SearchQuerySchema, {
|
|
35
|
+
ShareUrl: shareUrl
|
|
36
|
+
}));
|
|
37
|
+
const product = result.data
|
|
38
|
+
.map((item) => anyUnpack(item, SearchObjectSchema))
|
|
39
|
+
.filter(Boolean)[0];
|
|
40
|
+
|
|
41
|
+
if (!product) {
|
|
42
|
+
writeJson(response, 404, { error: "Product not found", shareUrl });
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
writeJson(response, 200, productSnapshot(product, shareUrl));
|
|
47
|
+
} catch (error) {
|
|
48
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
49
|
+
writeJson(response, 502, { error: "Could not load product", detail: message });
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
server.listen(port, () => {
|
|
54
|
+
console.log(`Orderly product snapshot server listening on http://localhost:${port}/orderly-product-snapshot`);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
function productSnapshot(product, fallbackShareUrl) {
|
|
58
|
+
return {
|
|
59
|
+
shareUrl: product.ShareURL?.URL || fallbackShareUrl,
|
|
60
|
+
title: product.Title || "Produkt",
|
|
61
|
+
brand: normalizedBrand(product.Brand),
|
|
62
|
+
description: product.Description || "",
|
|
63
|
+
priceText: formatCredit(product.Price),
|
|
64
|
+
imageUrl: storedImageUrl(product.Images?.[0])
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function normalizeShareUrl(value) {
|
|
69
|
+
const trimmed = value.trim();
|
|
70
|
+
if (!trimmed || trimmed.startsWith(ORDERLY_SHARE_URL_PREFIX) || /^https?:\/\//i.test(trimmed)) {
|
|
71
|
+
return trimmed;
|
|
72
|
+
}
|
|
73
|
+
return `${ORDERLY_SHARE_URL_PREFIX}${trimmed.replace(/^\/+/, "")}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function normalizedBrand(brand) {
|
|
77
|
+
const value = brand?.trim();
|
|
78
|
+
if (!value || value.toLowerCase() === "ukendt" || value.toUpperCase() === "DVD") {
|
|
79
|
+
return "";
|
|
80
|
+
}
|
|
81
|
+
return value;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function formatCredit(credit) {
|
|
85
|
+
if (!credit) {
|
|
86
|
+
return "";
|
|
87
|
+
}
|
|
88
|
+
const amount = Number(credit.Amount ?? 0);
|
|
89
|
+
const currency = credit.Currency || "";
|
|
90
|
+
const prefix = currency.toUpperCase() === "DKK" ? "kr." : currency.trim();
|
|
91
|
+
const value = amount.toLocaleString("da-DK", {
|
|
92
|
+
minimumFractionDigits: 2,
|
|
93
|
+
maximumFractionDigits: 2
|
|
94
|
+
});
|
|
95
|
+
return `${prefix ? `${prefix} ` : ""}${value}`.trim();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function storedImageUrl(image) {
|
|
99
|
+
if (!image) {
|
|
100
|
+
return "";
|
|
101
|
+
}
|
|
102
|
+
if (image.ImageDataBase64) {
|
|
103
|
+
return `data:image/jpeg;base64,${image.ImageDataBase64}`;
|
|
104
|
+
}
|
|
105
|
+
if (/^https?:\/\//i.test(image.Name) || image.Name.startsWith("/")) {
|
|
106
|
+
return image.Name;
|
|
107
|
+
}
|
|
108
|
+
if (!image.Name) {
|
|
109
|
+
return "";
|
|
110
|
+
}
|
|
111
|
+
const imageName = image.Name.toLowerCase();
|
|
112
|
+
const path = image.IsAsset || imageName.includes("cms") || imageName.startsWith("images")
|
|
113
|
+
? "objects"
|
|
114
|
+
: "thumbnails";
|
|
115
|
+
return joinUrl(imageBaseUrl, path, image.Name);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function joinUrl(baseUrl, ...parts) {
|
|
119
|
+
const root = String(baseUrl).endsWith("/") ? String(baseUrl) : `${baseUrl}/`;
|
|
120
|
+
return new URL(parts
|
|
121
|
+
.filter(Boolean)
|
|
122
|
+
.map((part) => String(part).replace(/^\/+|\/+$/g, ""))
|
|
123
|
+
.join("/"), root).toString();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function writeJson(response, status, value) {
|
|
127
|
+
const body = `${JSON.stringify(value)}\n`;
|
|
128
|
+
response.writeHead(status, {
|
|
129
|
+
"content-type": "application/json; charset=utf-8",
|
|
130
|
+
"cache-control": status === 200 ? "public, max-age=300" : "no-store"
|
|
131
|
+
});
|
|
132
|
+
response.end(body);
|
|
133
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
declare(strict_types=1);
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Orderly product SSR helper for PHP hosts.
|
|
6
|
+
*
|
|
7
|
+
* Deploy this file next to the built static shop output, including product.html.
|
|
8
|
+
* The script rewrites clean product URLs to a real HTML response, injects
|
|
9
|
+
* declarative SSR fallback content into <orderly-product-detail-page>, and then
|
|
10
|
+
* lets the browser web component hydrate the live product view.
|
|
11
|
+
*
|
|
12
|
+
* Optional environment variables:
|
|
13
|
+
* - ORDERLY_PRODUCT_TEMPLATE: path to built product.html. Defaults to ./product.html.
|
|
14
|
+
* - ORDERLY_PRODUCT_ORIGIN: prefix for compact product slugs. Defaults to https://orderly.shop/.
|
|
15
|
+
* - ORDERLY_PRODUCT_SNAPSHOT_ENDPOINT: internal JSON endpoint returning product snapshot data.
|
|
16
|
+
* - ORDERLY_PRODUCT_SNAPSHOT_TIMEOUT: snapshot endpoint timeout in seconds. Defaults to 2.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
$shareUrl = orderly_product_share_url();
|
|
20
|
+
$snapshot = orderly_load_product_snapshot($shareUrl);
|
|
21
|
+
$templatePath = getenv('ORDERLY_PRODUCT_TEMPLATE') ?: __DIR__ . '/product.html';
|
|
22
|
+
$html = is_file($templatePath)
|
|
23
|
+
? file_get_contents($templatePath)
|
|
24
|
+
: orderly_minimal_product_template();
|
|
25
|
+
|
|
26
|
+
if ($html === false) {
|
|
27
|
+
http_response_code(500);
|
|
28
|
+
header('Content-Type: text/plain; charset=UTF-8');
|
|
29
|
+
echo 'Could not read product template.';
|
|
30
|
+
exit;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
$html = orderly_inject_product_page($html, $shareUrl, $snapshot);
|
|
34
|
+
$html = orderly_inject_meta($html, $shareUrl, $snapshot);
|
|
35
|
+
|
|
36
|
+
header('Content-Type: text/html; charset=UTF-8');
|
|
37
|
+
echo $html;
|
|
38
|
+
|
|
39
|
+
function orderly_product_share_url(): string
|
|
40
|
+
{
|
|
41
|
+
$value = $_GET['url'] ?? '';
|
|
42
|
+
if (!is_string($value) || $value === '') {
|
|
43
|
+
$path = parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH);
|
|
44
|
+
$value = is_string($path) ? ltrim($path, '/') : '';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
$value = trim(rawurldecode($value));
|
|
48
|
+
if ($value === '') {
|
|
49
|
+
return '';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (preg_match('~^https?://~i', $value) === 1) {
|
|
53
|
+
return $value;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
$origin = getenv('ORDERLY_PRODUCT_ORIGIN') ?: 'https://orderly.shop/';
|
|
57
|
+
return rtrim($origin, '/') . '/' . ltrim($value, '/');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function orderly_load_product_snapshot(string $shareUrl): ?array
|
|
61
|
+
{
|
|
62
|
+
$endpoint = getenv('ORDERLY_PRODUCT_SNAPSHOT_ENDPOINT') ?: '';
|
|
63
|
+
if ($endpoint === '' || $shareUrl === '') {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
$separator = str_contains($endpoint, '?') ? '&' : '?';
|
|
68
|
+
$url = $endpoint . $separator . 'url=' . rawurlencode($shareUrl);
|
|
69
|
+
$timeout = (float) (getenv('ORDERLY_PRODUCT_SNAPSHOT_TIMEOUT') ?: '2');
|
|
70
|
+
$context = stream_context_create([
|
|
71
|
+
'http' => [
|
|
72
|
+
'method' => 'GET',
|
|
73
|
+
'timeout' => $timeout,
|
|
74
|
+
'header' => "Accept: application/json\r\n",
|
|
75
|
+
],
|
|
76
|
+
]);
|
|
77
|
+
$json = @file_get_contents($url, false, $context);
|
|
78
|
+
if ($json === false || $json === '') {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
$data = json_decode($json, true);
|
|
83
|
+
return is_array($data) ? $data : null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function orderly_inject_product_page(string $html, string $shareUrl, ?array $snapshot): string
|
|
87
|
+
{
|
|
88
|
+
$fallback = orderly_render_product_fallback($shareUrl, $snapshot);
|
|
89
|
+
$component = '<orderly-product-detail-page data-orderly-ssr share-url="' . orderly_e($shareUrl) . '">' . "\n"
|
|
90
|
+
. $fallback . "\n"
|
|
91
|
+
. '</orderly-product-detail-page>';
|
|
92
|
+
|
|
93
|
+
$result = preg_replace(
|
|
94
|
+
'~<orderly-product-detail-page\b[^>]*>.*?</orderly-product-detail-page>~is',
|
|
95
|
+
$component,
|
|
96
|
+
$html,
|
|
97
|
+
1,
|
|
98
|
+
$count
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
if ($result !== null && $count > 0) {
|
|
102
|
+
return $result;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return str_replace('</body>', $component . "\n</body>", $html);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function orderly_render_product_fallback(string $shareUrl, ?array $snapshot): string
|
|
109
|
+
{
|
|
110
|
+
$title = orderly_snapshot_value($snapshot, 'title') ?: orderly_title_from_share_url($shareUrl);
|
|
111
|
+
$brand = orderly_snapshot_value($snapshot, 'brand');
|
|
112
|
+
$price = orderly_snapshot_value($snapshot, 'priceText');
|
|
113
|
+
$description = orderly_snapshot_value($snapshot, 'description');
|
|
114
|
+
$imageUrl = orderly_snapshot_value($snapshot, 'imageUrl');
|
|
115
|
+
$titleMarkup = orderly_e($title);
|
|
116
|
+
|
|
117
|
+
$image = $imageUrl !== ''
|
|
118
|
+
? '<div class="orderly-ssr-product-detail__image"><img src="' . orderly_e($imageUrl) . '" alt="' . orderly_e($title) . '" loading="eager"></div>'
|
|
119
|
+
: '';
|
|
120
|
+
$brandMarkup = $brand !== '' ? '<p data-orderly-field="brand">' . orderly_e($brand) . '</p>' : '';
|
|
121
|
+
$priceMarkup = $price !== '' ? '<p data-orderly-field="price">' . orderly_e($price) . '</p>' : '';
|
|
122
|
+
$descriptionMarkup = $description !== '' ? '<p data-orderly-field="description">' . orderly_e($description) . '</p>' : '';
|
|
123
|
+
|
|
124
|
+
return <<<HTML
|
|
125
|
+
<article slot="content-before" class="orderly-ssr-product-detail" data-orderly-ssr-fallback data-orderly-product>
|
|
126
|
+
{$image}
|
|
127
|
+
<section class="orderly-ssr-product-detail__text">
|
|
128
|
+
{$brandMarkup}
|
|
129
|
+
<h1 data-orderly-field="title">{$titleMarkup}</h1>
|
|
130
|
+
{$priceMarkup}
|
|
131
|
+
{$descriptionMarkup}
|
|
132
|
+
</section>
|
|
133
|
+
</article>
|
|
134
|
+
HTML;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function orderly_inject_meta(string $html, string $shareUrl, ?array $snapshot): string
|
|
138
|
+
{
|
|
139
|
+
$title = orderly_snapshot_value($snapshot, 'title') ?: orderly_title_from_share_url($shareUrl);
|
|
140
|
+
$description = orderly_snapshot_value($snapshot, 'description');
|
|
141
|
+
$imageUrl = orderly_snapshot_value($snapshot, 'imageUrl');
|
|
142
|
+
|
|
143
|
+
$html = preg_replace('~<title>.*?</title>~is', '<title>' . orderly_e($title) . '</title>', $html, 1) ?? $html;
|
|
144
|
+
$html = orderly_upsert_head_tag($html, '~<meta\s+name=["\']description["\'][^>]*>~i', '<meta name="description" content="' . orderly_e($description ?: $title) . '">');
|
|
145
|
+
$html = orderly_upsert_head_tag($html, '~<link\s+rel=["\']canonical["\'][^>]*>~i', '<link rel="canonical" href="' . orderly_e($shareUrl) . '">');
|
|
146
|
+
$html = orderly_upsert_head_tag($html, '~<meta\s+property=["\']og:title["\'][^>]*>~i', '<meta property="og:title" content="' . orderly_e($title) . '">');
|
|
147
|
+
$html = orderly_upsert_head_tag($html, '~<meta\s+property=["\']og:url["\'][^>]*>~i', '<meta property="og:url" content="' . orderly_e($shareUrl) . '">');
|
|
148
|
+
if ($description !== '') {
|
|
149
|
+
$html = orderly_upsert_head_tag($html, '~<meta\s+property=["\']og:description["\'][^>]*>~i', '<meta property="og:description" content="' . orderly_e($description) . '">');
|
|
150
|
+
}
|
|
151
|
+
if ($imageUrl !== '') {
|
|
152
|
+
$html = orderly_upsert_head_tag($html, '~<meta\s+property=["\']og:image["\'][^>]*>~i', '<meta property="og:image" content="' . orderly_e($imageUrl) . '">');
|
|
153
|
+
}
|
|
154
|
+
return $html;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function orderly_upsert_head_tag(string $html, string $pattern, string $tag): string
|
|
158
|
+
{
|
|
159
|
+
if (preg_match($pattern, $html) === 1) {
|
|
160
|
+
return preg_replace($pattern, $tag, $html, 1) ?? $html;
|
|
161
|
+
}
|
|
162
|
+
return str_replace('</head>', ' ' . $tag . "\n </head>", $html);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function orderly_snapshot_value(?array $snapshot, string $key): string
|
|
166
|
+
{
|
|
167
|
+
$value = $snapshot[$key] ?? '';
|
|
168
|
+
return is_scalar($value) ? trim((string) $value) : '';
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function orderly_title_from_share_url(string $shareUrl): string
|
|
172
|
+
{
|
|
173
|
+
$path = parse_url($shareUrl, PHP_URL_PATH);
|
|
174
|
+
$name = is_string($path) ? basename($path, '.html') : 'Produkt';
|
|
175
|
+
$name = preg_replace('~-[a-z0-9]{8,}$~i', '', $name) ?? $name;
|
|
176
|
+
$name = str_replace(['-', '_'], ' ', $name);
|
|
177
|
+
return ucwords($name ?: 'Produkt');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function orderly_minimal_product_template(): string
|
|
181
|
+
{
|
|
182
|
+
return <<<'HTML'
|
|
183
|
+
<!doctype html>
|
|
184
|
+
<html lang="da">
|
|
185
|
+
<head>
|
|
186
|
+
<meta charset="UTF-8">
|
|
187
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
188
|
+
<title>Produkt</title>
|
|
189
|
+
<script src="/node_modules/@orderlyshop/web-components/dist/browser/orderly-web-components.global.js"></script>
|
|
190
|
+
<script>
|
|
191
|
+
OrderlyWebComponents.configureShop({
|
|
192
|
+
defaultShop: { baseUrl: "https://service.orderly.shop" }
|
|
193
|
+
});
|
|
194
|
+
</script>
|
|
195
|
+
</head>
|
|
196
|
+
<body></body>
|
|
197
|
+
</html>
|
|
198
|
+
HTML;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function orderly_e(string $value): string
|
|
202
|
+
{
|
|
203
|
+
return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
|
204
|
+
}
|