@bivabdas/sharq 1.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.
@@ -0,0 +1,298 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ function escapeHtml(value) {
5
+ return String(value).replace(/[&<>"']/g, (char) => {
6
+ const map = {
7
+ "&": "&amp;",
8
+ "<": "&lt;",
9
+ ">": "&gt;",
10
+ "\"": "&quot;",
11
+ "'": "&#39;"
12
+ };
13
+
14
+ return map[char];
15
+ });
16
+ }
17
+
18
+ function sanitizeHref(href) {
19
+ const normalizedHref = href.trim();
20
+ const loweredHref = normalizedHref.toLowerCase();
21
+
22
+ if (
23
+ loweredHref.startsWith("javascript:") ||
24
+ loweredHref.startsWith("data:") ||
25
+ loweredHref.startsWith("vbscript:")
26
+ ) {
27
+ return "#";
28
+ }
29
+
30
+ return normalizedHref;
31
+ }
32
+
33
+ function parseFrontmatter(markdown) {
34
+ if (!markdown.startsWith("---")) {
35
+ throw new Error("Missing frontmatter block.");
36
+ }
37
+
38
+ const match = markdown.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
39
+
40
+ if (!match) {
41
+ throw new Error("Invalid frontmatter format.");
42
+ }
43
+
44
+ const [, rawFrontmatter, content] = match;
45
+ const data = {};
46
+
47
+ for (const line of rawFrontmatter.split("\n")) {
48
+ if (!line.trim()) {
49
+ continue;
50
+ }
51
+
52
+ const separatorIndex = line.indexOf(":");
53
+
54
+ if (separatorIndex === -1) {
55
+ throw new Error(`Invalid frontmatter line: ${line}`);
56
+ }
57
+
58
+ const key = line.slice(0, separatorIndex).trim();
59
+ const value = line.slice(separatorIndex + 1).trim();
60
+ data[key] = value;
61
+ }
62
+
63
+ for (const field of ["title", "description", "date"]) {
64
+ if (!data[field]) {
65
+ throw new Error(`Missing frontmatter field: ${field}`);
66
+ }
67
+ }
68
+
69
+ if (data.tags) {
70
+ data.tags = data.tags
71
+ .split(",")
72
+ .map((tag) => tag.trim())
73
+ .filter(Boolean);
74
+ } else {
75
+ data.tags = [];
76
+ }
77
+
78
+ return { data, content: content.trim() };
79
+ }
80
+
81
+ function renderInlineMarkdown(text) {
82
+ const linkedText = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, label, href) => {
83
+ const safeLabel = escapeHtml(label);
84
+ const safeHref = escapeHtml(sanitizeHref(href));
85
+ return `<a href="${safeHref}">${safeLabel}</a>`;
86
+ });
87
+
88
+ return escapeHtml(linkedText)
89
+ .replace(/&lt;a href=&quot;(.+?)&quot;&gt;(.+?)&lt;\/a&gt;/g, '<a href="$1">$2</a>')
90
+ .replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>")
91
+ .replace(/\*(.+?)\*/g, "<em>$1</em>")
92
+ .replace(/`(.+?)`/g, "<code>$1</code>");
93
+ }
94
+
95
+ function markdownToHtml(markdown) {
96
+ const lines = markdown.split("\n");
97
+ const html = [];
98
+ let inList = false;
99
+ let listType = null;
100
+ let inCodeBlock = false;
101
+ let codeBuffer = [];
102
+
103
+ const flushList = () => {
104
+ if (inList) {
105
+ html.push(listType === "ol" ? "</ol>" : "</ul>");
106
+ inList = false;
107
+ listType = null;
108
+ }
109
+ };
110
+
111
+ const flushCodeBlock = () => {
112
+ if (inCodeBlock) {
113
+ html.push(`<pre><code>${escapeHtml(codeBuffer.join("\n"))}</code></pre>`);
114
+ inCodeBlock = false;
115
+ codeBuffer = [];
116
+ }
117
+ };
118
+
119
+ for (const line of lines) {
120
+ if (line.startsWith("```")) {
121
+ flushList();
122
+ if (inCodeBlock) {
123
+ flushCodeBlock();
124
+ } else {
125
+ inCodeBlock = true;
126
+ }
127
+ continue;
128
+ }
129
+
130
+ if (inCodeBlock) {
131
+ codeBuffer.push(line);
132
+ continue;
133
+ }
134
+
135
+ const trimmed = line.trim();
136
+
137
+ if (!trimmed) {
138
+ flushList();
139
+ continue;
140
+ }
141
+
142
+ if (trimmed.startsWith("# ")) {
143
+ flushList();
144
+ html.push(`<h1>${renderInlineMarkdown(trimmed.slice(2))}</h1>`);
145
+ continue;
146
+ }
147
+
148
+ if (trimmed.startsWith("## ")) {
149
+ flushList();
150
+ html.push(`<h2>${renderInlineMarkdown(trimmed.slice(3))}</h2>`);
151
+ continue;
152
+ }
153
+
154
+ if (trimmed.startsWith("### ")) {
155
+ flushList();
156
+ html.push(`<h3>${renderInlineMarkdown(trimmed.slice(4))}</h3>`);
157
+ continue;
158
+ }
159
+
160
+ if (trimmed.startsWith("- ")) {
161
+ if (!inList || listType !== "ul") {
162
+ flushList();
163
+ html.push("<ul>");
164
+ inList = true;
165
+ listType = "ul";
166
+ }
167
+ html.push(`<li>${renderInlineMarkdown(trimmed.slice(2))}</li>`);
168
+ continue;
169
+ }
170
+
171
+ if (/^\d+\.\s/.test(trimmed)) {
172
+ if (!inList || listType !== "ol") {
173
+ flushList();
174
+ html.push("<ol>");
175
+ inList = true;
176
+ listType = "ol";
177
+ }
178
+ html.push(`<li>${renderInlineMarkdown(trimmed.replace(/^\d+\.\s/, ""))}</li>`);
179
+ continue;
180
+ }
181
+
182
+ if (trimmed.startsWith("> ")) {
183
+ flushList();
184
+ html.push(`<blockquote><p>${renderInlineMarkdown(trimmed.slice(2))}</p></blockquote>`);
185
+ continue;
186
+ }
187
+
188
+ flushList();
189
+ html.push(`<p>${renderInlineMarkdown(trimmed)}</p>`);
190
+ }
191
+
192
+ flushList();
193
+ flushCodeBlock();
194
+
195
+ return html.join("\n");
196
+ }
197
+
198
+ function stripDuplicateTitleHeading(markdown, title) {
199
+ const lines = markdown.split("\n");
200
+ const firstContentLine = lines.findIndex((line) => line.trim());
201
+
202
+ if (firstContentLine === -1) {
203
+ return markdown;
204
+ }
205
+
206
+ const firstLine = lines[firstContentLine].trim();
207
+ if (firstLine === `# ${title}`) {
208
+ lines.splice(firstContentLine, 1);
209
+ }
210
+
211
+ return lines.join("\n").trim();
212
+ }
213
+
214
+ function applyTemplate(template, variables) {
215
+ return template.replace(/\{\{(\w+)\}\}/g, (_, key) => variables[key] ?? "");
216
+ }
217
+
218
+ export async function loadTemplate(config, name) {
219
+ const templatePath = path.join(config.templatesDir, name);
220
+ return fs.readFile(templatePath, "utf8");
221
+ }
222
+
223
+ export async function renderPostPage(config, post) {
224
+ const template = await loadTemplate(config, "post.html");
225
+ const cleanedContent = stripDuplicateTitleHeading(post.content, post.title);
226
+ const tagsMarkup = post.tags
227
+ .map((tag) => `<li class="tag">${escapeHtml(tag)}</li>`)
228
+ .join("");
229
+
230
+ return applyTemplate(template, {
231
+ title: escapeHtml(post.title),
232
+ description: escapeHtml(post.description),
233
+ date: escapeHtml(post.date),
234
+ author: escapeHtml(post.author || "sharq"),
235
+ tags: tagsMarkup,
236
+ content: markdownToHtml(cleanedContent)
237
+ });
238
+ }
239
+
240
+ export async function renderIndexPage(config, posts) {
241
+ const template = await loadTemplate(config, "index.html");
242
+ const items = posts
243
+ .map(
244
+ (post) =>
245
+ `<li><div><a href="/blog/${post.slug}">${escapeHtml(post.title)}</a><p>${escapeHtml(post.description)}</p></div><span>${escapeHtml(post.date)}</span></li>`
246
+ )
247
+ .join("\n");
248
+
249
+ return applyTemplate(template, {
250
+ title: escapeHtml(config.siteTitle),
251
+ description: escapeHtml(config.siteDescription),
252
+ date: "",
253
+ content: items || "<li>No posts available yet.</li>"
254
+ });
255
+ }
256
+
257
+ export async function renderArchivePage(config, posts) {
258
+ const template = await loadTemplate(config, "archive.html");
259
+ const items = posts
260
+ .map((post) => {
261
+ const tagMarkup = post.tags.map((tag) => `<li class="tag">${escapeHtml(tag)}</li>`).join("");
262
+ return `
263
+ <article class="archive-item">
264
+ <div class="archive-heading">
265
+ <p class="eyebrow">${escapeHtml(post.date)}</p>
266
+ <h2><a href="/blog/${post.slug}">${escapeHtml(post.title)}</a></h2>
267
+ </div>
268
+ <p>${escapeHtml(post.description)}</p>
269
+ <p class="archive-meta">By ${escapeHtml(post.author || "sharq")}</p>
270
+ <ul class="tag-list">${tagMarkup}</ul>
271
+ </article>
272
+ `;
273
+ })
274
+ .join("\n");
275
+
276
+ return applyTemplate(template, {
277
+ title: `Archive | ${escapeHtml(config.siteTitle)}`,
278
+ description: escapeHtml(`Browse all posts on ${config.siteTitle}`),
279
+ date: "",
280
+ content: items || "<p>No posts available yet.</p>"
281
+ });
282
+ }
283
+
284
+ export async function readPostSource(config, slug) {
285
+ const filePath = path.join(config.contentDir, `${slug}.md`);
286
+ const markdown = await fs.readFile(filePath, "utf8");
287
+ const { data, content } = parseFrontmatter(markdown);
288
+
289
+ return {
290
+ slug,
291
+ title: data.title,
292
+ description: data.description,
293
+ date: data.date,
294
+ author: data.author || "",
295
+ tags: data.tags,
296
+ content
297
+ };
298
+ }
@@ -0,0 +1,161 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import readline from "node:readline/promises";
4
+ import { stdin as input, stdout as output } from "node:process";
5
+
6
+ function toSlug(value) {
7
+ return value
8
+ .trim()
9
+ .toLowerCase()
10
+ .replace(/[^a-z0-9]+/g, "-")
11
+ .replace(/^-+|-+$/g, "");
12
+ }
13
+
14
+ function titleFromName(value) {
15
+ return value
16
+ .split(/[-_\s]+/)
17
+ .filter(Boolean)
18
+ .map((part) => part[0].toUpperCase() + part.slice(1))
19
+ .join(" ");
20
+ }
21
+
22
+ async function ensureEmptyOrNewDirectory(projectDir) {
23
+ try {
24
+ const entries = await fs.readdir(projectDir);
25
+ if (entries.length > 0) {
26
+ throw new Error(`Target directory is not empty: ${projectDir}`);
27
+ }
28
+ } catch (error) {
29
+ if (error.code === "ENOENT") {
30
+ await fs.mkdir(projectDir, { recursive: true });
31
+ return;
32
+ }
33
+
34
+ throw error;
35
+ }
36
+ }
37
+
38
+ async function copyDir(sourceDir, targetDir) {
39
+ await fs.mkdir(targetDir, { recursive: true });
40
+ const entries = await fs.readdir(sourceDir, { withFileTypes: true });
41
+
42
+ for (const entry of entries) {
43
+ const sourcePath = path.join(sourceDir, entry.name);
44
+ const targetPath = path.join(targetDir, entry.name);
45
+
46
+ if (entry.isDirectory()) {
47
+ await copyDir(sourcePath, targetPath);
48
+ } else {
49
+ await fs.copyFile(sourcePath, targetPath);
50
+ }
51
+ }
52
+ }
53
+
54
+ function normalizeDependencySpec(spec) {
55
+ if (!spec) {
56
+ return "latest";
57
+ }
58
+
59
+ return spec;
60
+ }
61
+
62
+ function resolveLocalDependencySpec(projectDir, packageRoot) {
63
+ const relativePath = path.relative(projectDir, packageRoot) || ".";
64
+ return `file:${relativePath}`;
65
+ }
66
+
67
+ async function writePackageJson(projectDir, projectName, frameworkDependency) {
68
+ const packageJson = {
69
+ name: projectName,
70
+ version: "0.1.0",
71
+ private: true,
72
+ type: "module",
73
+ scripts: {
74
+ dev: "sharq dev",
75
+ start: "sharq start",
76
+ build: "sharq build"
77
+ },
78
+ dependencies: {
79
+ sharq: frameworkDependency
80
+ }
81
+ };
82
+
83
+ await fs.writeFile(path.join(projectDir, "package.json"), `${JSON.stringify(packageJson, null, 2)}\n`, "utf8");
84
+ }
85
+
86
+ async function writeConfig(projectDir, { siteTitle, siteDescription }) {
87
+ const configSource = `export default {
88
+ siteTitle: ${JSON.stringify(siteTitle)},
89
+ siteDescription: ${JSON.stringify(siteDescription)}
90
+ };
91
+ `;
92
+
93
+ await fs.writeFile(path.join(projectDir, "sharq.config.js"), configSource, "utf8");
94
+ }
95
+
96
+ async function writeReadme(projectDir, { projectName }) {
97
+ const readme = `# ${projectName}
98
+
99
+ Generated with sharq.
100
+
101
+ ## Scripts
102
+
103
+ - \`npm run dev\`
104
+ - \`npm start\`
105
+ - \`npm run build\`
106
+
107
+ ## Structure
108
+
109
+ - \`content/\` markdown posts
110
+ - \`templates/\` page templates
111
+ - \`public/assets/\` static assets
112
+ - \`sharq/\` production build output
113
+ - \`sharq.config.js\` project-level settings
114
+ `;
115
+
116
+ await fs.writeFile(path.join(projectDir, "README.md"), readme, "utf8");
117
+ }
118
+
119
+ export async function scaffoldProject(options = {}) {
120
+ const packageRoot = options.packageRoot;
121
+
122
+ let answers = {
123
+ projectName: options.projectName || "",
124
+ siteTitle: options.siteTitle || "",
125
+ siteDescription: options.siteDescription || ""
126
+ };
127
+
128
+ if (!answers.projectName) {
129
+ const rl = readline.createInterface({ input, output });
130
+ const requestedName = await rl.question("Project name: ");
131
+ const projectName = toSlug(requestedName) || "my-sharq-site";
132
+ const siteTitleInput = await rl.question(`Site title (${titleFromName(projectName)}): `);
133
+ const siteDescriptionInput = await rl.question("Site description (A sharq site): ");
134
+ rl.close();
135
+
136
+ answers = {
137
+ projectName,
138
+ siteTitle: siteTitleInput.trim() || titleFromName(projectName),
139
+ siteDescription: siteDescriptionInput.trim() || "A sharq site"
140
+ };
141
+ } else {
142
+ answers.projectName = toSlug(answers.projectName);
143
+ answers.siteTitle = answers.siteTitle || titleFromName(answers.projectName);
144
+ answers.siteDescription = answers.siteDescription || "A sharq site";
145
+ }
146
+
147
+ const projectDir = path.resolve(process.cwd(), answers.projectName);
148
+ await ensureEmptyOrNewDirectory(projectDir);
149
+
150
+ const frameworkDependency = options.useLocalDependency
151
+ ? resolveLocalDependencySpec(projectDir, packageRoot)
152
+ : normalizeDependencySpec(options.frameworkVersion);
153
+
154
+ const templateDir = path.join(packageRoot, "scaffold", "template");
155
+ await copyDir(templateDir, projectDir);
156
+ await writePackageJson(projectDir, answers.projectName, frameworkDependency);
157
+ await writeConfig(projectDir, answers);
158
+ await writeReadme(projectDir, answers);
159
+
160
+ return { projectDir, frameworkDependency, ...answers };
161
+ }
package/lib/server.js ADDED
@@ -0,0 +1,199 @@
1
+ import fs from "node:fs/promises";
2
+ import http from "node:http";
3
+ import path from "node:path";
4
+ import { resolveConfig } from "./config.js";
5
+ import { ensurePostGenerated, refreshSiteArtifacts } from "./generator.js";
6
+
7
+ const contentTypes = {
8
+ ".html": "text/html; charset=utf-8",
9
+ ".xml": "application/xml; charset=utf-8",
10
+ ".css": "text/css; charset=utf-8",
11
+ ".js": "application/javascript; charset=utf-8",
12
+ ".json": "application/json; charset=utf-8",
13
+ ".svg": "image/svg+xml",
14
+ ".png": "image/png",
15
+ ".jpg": "image/jpeg",
16
+ ".jpeg": "image/jpeg",
17
+ ".webp": "image/webp",
18
+ ".ico": "image/x-icon",
19
+ ".txt": "text/plain; charset=utf-8"
20
+ };
21
+
22
+ async function sendFile(response, filePath) {
23
+ const extension = path.extname(filePath);
24
+ const body = await fs.readFile(filePath);
25
+ response.writeHead(200, { "Content-Type": contentTypes[extension] || "text/plain; charset=utf-8" });
26
+ response.end(body);
27
+ }
28
+
29
+ function sendHtml(response, statusCode, html) {
30
+ response.writeHead(statusCode, { "Content-Type": "text/html; charset=utf-8" });
31
+ response.end(html);
32
+ }
33
+
34
+ function logRequest(method, pathname, statusCode) {
35
+ console.log(`[sharq] ${method} ${pathname} -> ${statusCode}`);
36
+ }
37
+
38
+ function notFound(response, message = "Page not found.") {
39
+ sendHtml(response, 404, `<!doctype html><html><body><h1>404</h1><p>${message}</p></body></html>`);
40
+ }
41
+
42
+ async function servePublicPath(config, response, pathname) {
43
+ const relativePath = pathname.replace(/^\/+/, "");
44
+ const filePath = path.resolve(config.publicDir, relativePath);
45
+
46
+ if (!filePath.startsWith(config.publicDir)) {
47
+ throw new Error("Invalid public asset path.");
48
+ }
49
+
50
+ await sendFile(response, filePath);
51
+ }
52
+
53
+ function createApp(config) {
54
+ let isReady = false;
55
+
56
+ async function handleRequest(request, response) {
57
+ if (!request.url) {
58
+ notFound(response);
59
+ return;
60
+ }
61
+
62
+ const url = new URL(request.url, config.siteUrl);
63
+ const { pathname } = url;
64
+
65
+ try {
66
+ if (!isReady) {
67
+ sendHtml(
68
+ response,
69
+ 503,
70
+ "<!doctype html><html><body><h1>503</h1><p>sharq is still preparing generated files.</p></body></html>"
71
+ );
72
+ logRequest(request.method || "GET", pathname, 503);
73
+ return;
74
+ }
75
+
76
+ if (pathname === "/") {
77
+ await refreshSiteArtifacts(config);
78
+ await sendFile(response, config.homePagePath);
79
+ logRequest(request.method || "GET", pathname, 200);
80
+ return;
81
+ }
82
+
83
+ if (pathname === "/sitemap.xml") {
84
+ await refreshSiteArtifacts(config);
85
+ await sendFile(response, config.sitemapPath);
86
+ logRequest(request.method || "GET", pathname, 200);
87
+ return;
88
+ }
89
+
90
+ if (pathname === "/archive") {
91
+ await refreshSiteArtifacts(config);
92
+ await sendFile(response, config.archivePagePath);
93
+ logRequest(request.method || "GET", pathname, 200);
94
+ return;
95
+ }
96
+
97
+ if (pathname.startsWith("/blog/")) {
98
+ const slug = pathname.replace("/blog/", "").replace(/\/+$/, "");
99
+
100
+ if (!slug) {
101
+ notFound(response, "Missing blog slug.");
102
+ return;
103
+ }
104
+
105
+ const filePath = await ensurePostGenerated(config, slug);
106
+ await sendFile(response, filePath);
107
+ logRequest(request.method || "GET", pathname, 200);
108
+ return;
109
+ }
110
+
111
+ if (pathname.startsWith("/assets/")) {
112
+ await servePublicPath(config, response, pathname);
113
+ logRequest(request.method || "GET", pathname, 200);
114
+ return;
115
+ }
116
+
117
+ notFound(response);
118
+ logRequest(request.method || "GET", pathname, 404);
119
+ } catch (error) {
120
+ if (error && error.code === "ENOENT") {
121
+ notFound(response, "Requested content does not exist.");
122
+ logRequest(request.method || "GET", pathname, 404);
123
+ return;
124
+ }
125
+
126
+ if (error instanceof Error && error.message === "Invalid slug format.") {
127
+ notFound(response, "Invalid blog slug.");
128
+ logRequest(request.method || "GET", pathname, 404);
129
+ return;
130
+ }
131
+
132
+ if (error instanceof Error && error.message === "Invalid public asset path.") {
133
+ notFound(response, "Invalid asset path.");
134
+ logRequest(request.method || "GET", pathname, 404);
135
+ return;
136
+ }
137
+
138
+ sendHtml(
139
+ response,
140
+ 500,
141
+ `<!doctype html><html><body><h1>500</h1><p>${String(error.message || error)}</p></body></html>`
142
+ );
143
+ logRequest(request.method || "GET", pathname, 500);
144
+ }
145
+ }
146
+
147
+ async function prepare() {
148
+ await refreshSiteArtifacts(config);
149
+ isReady = true;
150
+ }
151
+
152
+ function createServer() {
153
+ return http.createServer((request, response) => {
154
+ handleRequest(request, response);
155
+ });
156
+ }
157
+
158
+ return { prepare, createServer };
159
+ }
160
+
161
+ function listenOnAvailablePort(createServerInstance, port, attemptsLeft) {
162
+ return new Promise((resolve, reject) => {
163
+ const server = createServerInstance();
164
+
165
+ const handleError = (error) => {
166
+ server.close();
167
+
168
+ if (error.code === "EADDRINUSE" && attemptsLeft > 0) {
169
+ resolve(listenOnAvailablePort(createServerInstance, port + 1, attemptsLeft - 1));
170
+ return;
171
+ }
172
+
173
+ reject(error);
174
+ };
175
+
176
+ server.once("error", handleError);
177
+ server.listen(port, () => {
178
+ server.off("error", handleError);
179
+ resolve({ server, port });
180
+ });
181
+ });
182
+ }
183
+
184
+ export async function startServer(options = {}) {
185
+ const rootDir = options.rootDir || process.cwd();
186
+ const config = await resolveConfig(rootDir);
187
+ const requestedPort = Number(options.port || process.env.PORT || 3000);
188
+ const maxPortAttempts = Number(options.maxPortAttempts || 20);
189
+ const app = createApp(config);
190
+ const { server, port } = await listenOnAvailablePort(() => app.createServer(), requestedPort, maxPortAttempts);
191
+
192
+ if (!process.env.SITE_URL) {
193
+ config.siteUrl = `http://localhost:${port}`;
194
+ }
195
+
196
+ await app.prepare();
197
+ console.log(`sharq running at http://localhost:${port}`);
198
+ return { server, port, config };
199
+ }
package/lib/sitemap.js ADDED
@@ -0,0 +1,21 @@
1
+ import fs from "node:fs/promises";
2
+
3
+ function buildUrl(config, pathname) {
4
+ return `${config.siteUrl}${pathname}`;
5
+ }
6
+
7
+ export async function generateSitemap(config, posts) {
8
+ const urls = [
9
+ buildUrl(config, "/"),
10
+ buildUrl(config, "/archive"),
11
+ ...posts.map((post) => buildUrl(config, `/blog/${post.slug}`))
12
+ ];
13
+
14
+ const xml = `<?xml version="1.0" encoding="UTF-8"?>
15
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
16
+ ${urls.map((url) => ` <url><loc>${url}</loc></url>`).join("\n")}
17
+ </urlset>
18
+ `;
19
+
20
+ await fs.writeFile(config.sitemapPath, xml, "utf8");
21
+ }