@portosaur/core 0.1.4 → 0.2.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@portosaur/core",
3
- "version": "0.1.4",
3
+ "version": "0.2.0",
4
4
  "description": "The engine of portosaur that translates YAML configuration into Docusaurus structures.",
5
5
  "license": "GPL-3.0-only",
6
6
  "author": "soymadip",
@@ -27,9 +27,12 @@
27
27
  },
28
28
  "types": "./src/index.d.ts",
29
29
  "dependencies": {
30
- "@portosaur/logger": "^0.1.4",
30
+ "@portosaur/logger": "^0.2.0",
31
31
  "favicons": "^7.2.0",
32
32
  "js-yaml": "^4.1.1",
33
33
  "sharp": "^0.34.5"
34
+ },
35
+ "engines": {
36
+ "node": ">=24.15.0"
34
37
  }
35
38
  }
package/src/app.mjs CHANGED
@@ -1,25 +1,25 @@
1
1
  import { readFileSync } from "fs";
2
- import { resolve } from "path";
2
+ import { fileURLToPath } from "url";
3
3
 
4
4
  /**
5
5
  * Portosaur application metadata and configuration
6
6
  */
7
7
  export const porto = (() => {
8
8
  try {
9
- const pkgPath = resolve(import.meta.dirname, "../../../package.json");
9
+ const pkgPath = fileURLToPath(new URL("../package.json", import.meta.url));
10
10
  const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
11
11
 
12
12
  return {
13
- name: pkg.name || "Portosaur",
14
- version: pkg.version || "0.0.0",
15
- description: pkg.description || "",
16
- license: pkg.license || "",
17
- homepage: pkg.homepage || "",
18
- repository: pkg.repository?.url || "",
19
- engines: pkg.engines || {},
13
+ name: pkg.name,
14
+ version: pkg.version,
15
+ description: pkg.description,
16
+ license: pkg.license,
17
+ homepage: pkg.homepage,
18
+ repository: pkg.repository?.url,
19
+ engines: pkg.engines,
20
20
 
21
21
  // Derived/computed fields
22
- engineName: `${pkg.name || "Portosaur"}`,
22
+ engineName: pkg.name,
23
23
  };
24
24
  } catch (error) {
25
25
  console.warn(
@@ -1,5 +1,6 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
+ import { createRequire } from "module";
3
4
  import { getGitDate } from "../utils/system.mjs";
4
5
  import { porto } from "../app.mjs";
5
6
  import { resolveVars, getNestedValue } from "../utils/config.mjs";
@@ -15,19 +16,10 @@ import {
15
16
  /**
16
17
  * Generates a Docusaurus configuration object from raw user config
17
18
  */
18
- export function generateDocusaurusConfig(
19
- rawUserConfig,
20
- projectDir,
21
- context = {},
22
- ) {
23
- const {
24
- portoPkg = {},
25
- portoPaths = {},
26
- gitDate = null,
27
- env = process.env,
28
- } = context;
29
-
30
- const portoVersion = portoPkg.version ?? "0.0.0";
19
+ export function buildDocuConfig(rawUserConfig, projectDir, context = {}) {
20
+ const { portoPaths = {}, gitDate = null, env = process.env } = context;
21
+
22
+ const portoVersion = porto.version ?? "0.0.0";
31
23
  const lastUpdated = gitDate ?? getGitDate(projectDir);
32
24
 
33
25
  const staticDir = path.resolve(projectDir, "static");
@@ -65,6 +57,16 @@ export function generateDocusaurusConfig(
65
57
 
66
58
  // ------- Configuration Setup -------
67
59
 
60
+ // Collect static directories: local site static/, theme assets/, and portosaur dot-dir.
61
+ const staticDirectories = [
62
+ "static",
63
+ assetsDir,
64
+ path.join(projectDir, ".docusaurus", "portosaur"),
65
+ ].filter((dir) => dir && fs.existsSync(dir));
66
+
67
+ const isDarkMode = get("theme.appearance.dark_mode", true);
68
+ const disableSwitch = get("theme.appearance.disable_switch", false);
69
+
68
70
  return {
69
71
  projectName: siteName,
70
72
  title: siteName,
@@ -83,6 +85,104 @@ export function generateDocusaurusConfig(
83
85
  onBrokenLinks: get("site.on_broken_links", "throw"),
84
86
  i18n: { defaultLocale: "en", locales: ["en"] },
85
87
 
88
+ staticDirectories,
89
+
90
+ themeConfig: {
91
+ colorMode: {
92
+ defaultMode: isDarkMode ? "dark" : "light",
93
+ disableSwitch,
94
+ respectPrefersColorScheme: false,
95
+ },
96
+
97
+ navbar: {
98
+ title: siteName,
99
+ logo: {
100
+ alt: `${siteName} logo`,
101
+ src: resolveAsset(get("site.favicon", ""), "img/icon.png"),
102
+ },
103
+ hideOnScroll: get("theme.navigation.hide_navbar_on_scroll", true),
104
+ items: [
105
+ {
106
+ type: "search",
107
+ position: "right",
108
+ className: "navbar-search-bar",
109
+ },
110
+ ...(get("home_page.about.enable", true)
111
+ ? [
112
+ {
113
+ label: "About Me",
114
+ to: "/#about",
115
+ position: "right",
116
+ activeBasePath: "/never-match",
117
+ },
118
+ ]
119
+ : []),
120
+ ...(get("home_page.project_shelf.enable", true)
121
+ ? [
122
+ {
123
+ label: "Projects",
124
+ to: "/#projects",
125
+ position: "right",
126
+ activeBasePath: "/never-match",
127
+ },
128
+ ]
129
+ : []),
130
+ ...(get("home_page.experience.enable", false)
131
+ ? [
132
+ {
133
+ label: "Experience",
134
+ to: "/#experience",
135
+ position: "right",
136
+ activeBasePath: "/never-match",
137
+ },
138
+ ]
139
+ : []),
140
+ ...(get("home_page.social.enable", true)
141
+ ? [
142
+ {
143
+ label: "Contact",
144
+ to: "/#contact",
145
+ position: "right",
146
+ activeBasePath: "/never-match",
147
+ },
148
+ ]
149
+ : []),
150
+ {
151
+ type: "dropdown",
152
+ label: "More",
153
+ position: "right",
154
+ className: "_navbar-more-items",
155
+ items: [
156
+ { label: "Notes", to: "/notes" },
157
+ { label: "Blog", to: "/blog" },
158
+ ...(get("tasks.enable", false)
159
+ ? [{ label: "Tasks", to: "/tasks" }]
160
+ : []),
161
+ ...(!get("theme.appearance.disable_branding", false)
162
+ ? [
163
+ {
164
+ label: `Portosaur v${portoVersion}`,
165
+ className: "_nav-portosaur-version",
166
+ href:
167
+ porto?.homepage ||
168
+ "https://github.com/soymadip/portosaur",
169
+ },
170
+ ]
171
+ : []),
172
+ ],
173
+ },
174
+ ],
175
+ },
176
+
177
+ footer: {
178
+ style: "dark",
179
+ copyright: get(
180
+ "site.footer_text",
181
+ `© ${new Date().getFullYear()} ${siteName}. Built with Portosaur.`,
182
+ ),
183
+ },
184
+ },
185
+
86
186
  headTags: buildHeadTags([
87
187
  { meta: { name: "generator", content: `Portosaur v${porto.version}` } },
88
188
  { meta: { name: "theme-color", content: "var(--ifm-background-color)" } },
@@ -132,11 +232,17 @@ export function generateDocusaurusConfig(
132
232
  subtitle: get("home_page.hero.subtitle", "I am a"),
133
233
  profession: get("home_page.hero.profession", "Your Profession"),
134
234
  desc: get("home_page.hero.desc", "Welcome to my portfolio."),
235
+ social: get("home_page.hero.social", []),
236
+ learnMoreButtonTxt: get(
237
+ "home_page.hero.learn_more_button_txt",
238
+ "Learn More",
239
+ ),
135
240
  },
136
241
 
137
242
  aboutSection: {
138
243
  enable: get("home_page.about.enable", true),
139
244
  heading: get("home_page.about.heading", "About Me"),
245
+ name: get("site.title", "Your Name"),
140
246
  image: resolveAsset(get("home_page.about.image", "")),
141
247
  bio: get("home_page.about.bio", []),
142
248
  skills: get("home_page.about.skills", []),
@@ -194,20 +300,47 @@ export function generateDocusaurusConfig(
194
300
  path: "notes",
195
301
  sidebarPath: path.resolve(
196
302
  portoPaths.theme ?? context.portoRoot ?? "",
197
- "theme/config/sidebar.js",
303
+ "config/sidebar.jsx",
198
304
  ),
199
305
  },
200
306
  blog: { path: "blog", showReadingTime: false },
201
307
  theme: {
202
308
  customCss: path.resolve(
203
309
  portoPaths.theme ?? context.portoRoot ?? "",
204
- "theme/css/custom.css",
310
+ "css/custom.css",
205
311
  ),
206
312
  },
207
313
  },
208
314
  ],
209
315
  ],
210
316
 
317
+ // ------- Themes -------
318
+
319
+ themes: [
320
+ [
321
+ (() => {
322
+ const require = createRequire(import.meta.url);
323
+ return require.resolve("@easyops-cn/docusaurus-search-local", {
324
+ paths: [portoPaths.theme ?? context.portoRoot ?? ""],
325
+ });
326
+ })(),
327
+ {
328
+ hashed: true,
329
+ indexDocs: true,
330
+ indexBlog: true,
331
+ indexPages: true,
332
+ docsDir: "notes",
333
+ docsRouteBasePath: "notes",
334
+ searchContextByPaths: ["notes", "blog"],
335
+ highlightSearchTermsOnTargetPage: true,
336
+ explicitSearchResultPath: true,
337
+ hideSearchBarWithNoSearchContext: true,
338
+ searchBarShortcutHint: false,
339
+ language: ["en"],
340
+ },
341
+ ],
342
+ ],
343
+
211
344
  // ------- Plugins -------
212
345
 
213
346
  plugins: [
@@ -217,12 +350,25 @@ export function generateDocusaurusConfig(
217
350
  debug: !env.NODE_ENV || env.NODE_ENV === "development",
218
351
  offlineModeActivationStrategies: [
219
352
  "always",
220
- "deviceRetroactive",
221
- "query",
222
- "checkRedirect",
353
+ "appInstalled",
354
+ "queryString",
355
+ "standalone",
223
356
  ],
224
357
  },
225
358
  ],
359
+
360
+ // Serve the theme's pages/ directory as page routes.
361
+ // This registers pages/index.jsx → / and pages/tasks.jsx → /tasks etc.
362
+ [
363
+ "@docusaurus/plugin-content-pages",
364
+ {
365
+ id: "portosaur-pages",
366
+ path: path.resolve(
367
+ portoPaths.theme ?? context.portoRoot ?? "",
368
+ "pages",
369
+ ),
370
+ },
371
+ ],
226
372
  ],
227
373
  };
228
374
  }
@@ -45,6 +45,36 @@ function processManifest(manifestFile, outputDir, appVersion) {
45
45
  }
46
46
  }
47
47
 
48
+ /**
49
+ * Converts a raw HTML tag string from the favicons library into a Docusaurus
50
+ * headTag object: { tagName, attributes }.
51
+ *
52
+ * Example input: '<link rel="icon" href="/favicon/favicon.ico">'
53
+ * Example output: { tagName: 'link', attributes: { rel: 'icon', href: '/favicon/favicon.ico' } }
54
+ */
55
+ function parseHtmlTagString(htmlString) {
56
+ const tagMatch = htmlString.match(/^<(\w+)/);
57
+ if (!tagMatch) {
58
+ return null;
59
+ }
60
+
61
+ const tagName = tagMatch[1];
62
+ const attributes = {};
63
+ const attrRegex = /([\w-]+)(?:=["']([^"']*)["'])?/g;
64
+
65
+ // Skip past the opening tag name to only match attributes
66
+ attrRegex.lastIndex = tagName.length + 1;
67
+ let match;
68
+ while ((match = attrRegex.exec(htmlString)) !== null) {
69
+ if (match[0] === tagName) {
70
+ continue;
71
+ }
72
+ attributes[match[1]] = match[2] ?? "";
73
+ }
74
+
75
+ return { tagName, attributes };
76
+ }
77
+
48
78
  // Main Generation Function
49
79
 
50
80
  export async function generateFavicons(siteDir, options = {}) {
@@ -77,9 +107,16 @@ export async function generateFavicons(siteDir, options = {}) {
77
107
  if (fs.existsSync(hashFilePath)) {
78
108
  const existingHash = fs.readFileSync(hashFilePath, "utf-8");
79
109
  if (existingHash === configHash) {
110
+ const htmlFilePath = path.join(outputDir, ".favicon.html");
80
111
  if (fs.existsSync(path.join(outputDir, "favicon.ico"))) {
81
112
  logger.info("Favicons are up to date, skipping generation.");
82
- return { success: true, html: [] };
113
+ try {
114
+ const cachedHtml = JSON.parse(fs.readFileSync(htmlFilePath, "utf-8"));
115
+ const headTags = cachedHtml.map(parseHtmlTagString).filter(Boolean);
116
+ return { success: true, html: headTags };
117
+ } catch (e) {
118
+ return { success: true, html: [] };
119
+ }
83
120
  }
84
121
  }
85
122
  }
@@ -97,7 +134,10 @@ export async function generateFavicons(siteDir, options = {}) {
97
134
  const iconsToGenerate = ["note", "blog"];
98
135
  for (const icon of iconsToGenerate) {
99
136
  try {
100
- await extractSvg(icon, imgDir, iconColor);
137
+ await extractSvg(icon, imgDir, {
138
+ ...iconColor,
139
+ assetsDir: options.portoAssetsDir,
140
+ });
101
141
  } catch (e) {}
102
142
  }
103
143
 
@@ -212,7 +252,10 @@ export async function generateFavicons(siteDir, options = {}) {
212
252
  const htmlFilePath = path.join(outputDir, ".favicon.html");
213
253
  fs.writeFileSync(htmlFilePath, JSON.stringify(response.html), "utf-8");
214
254
  tempFiles.forEach(cleanupFile);
215
- return { success: true, html: response.html || [] };
255
+ const headTags = (response.html || [])
256
+ .map(parseHtmlTagString)
257
+ .filter(Boolean);
258
+ return { success: true, html: headTags };
216
259
  } catch (error) {
217
260
  logger.warn(`Favicon generation skipped: ${error.message}`);
218
261
  tempFiles.forEach(cleanupFile);
@@ -220,7 +263,8 @@ export async function generateFavicons(siteDir, options = {}) {
220
263
  if (fs.existsSync(htmlFilePath)) {
221
264
  try {
222
265
  const cachedHtml = JSON.parse(fs.readFileSync(htmlFilePath, "utf-8"));
223
- return { success: false, html: cachedHtml };
266
+ const headTags = cachedHtml.map(parseHtmlTagString).filter(Boolean);
267
+ return { success: false, html: headTags };
224
268
  } catch (e) {}
225
269
  }
226
270
  return { success: false, html: [] };
package/src/index.d.ts CHANGED
@@ -34,7 +34,6 @@ export { porto, git, text, limits } from "./app.mjs";
34
34
  export { generateFavicons } from "./generators/generateFavicons.mjs";
35
35
  export { generateRobotsTxt } from "./generators/generateRobots.mjs";
36
36
  export {
37
- generateDocusaurusConfig,
38
37
  buildDocuConfig,
39
38
  resolveSiteUrl,
40
39
  resolveBasePath,
package/src/index.mjs CHANGED
@@ -16,7 +16,7 @@ export * from "./app.mjs";
16
16
 
17
17
  export { generateFavicons } from "./generators/generateFavicons.mjs";
18
18
  export { generateRobotsTxt } from "./generators/generateRobots.mjs";
19
- export { generateDocusaurusConfig } from "./generators/docusaurusConfig.mjs";
19
+ export { buildDocuConfig } from "./generators/docusaurusConfig.mjs";
20
20
 
21
21
  export {
22
22
  resolveSiteUrl,
@@ -18,22 +18,26 @@ export function getNestedValue(obj, pathStr, ...fallbacks) {
18
18
  }
19
19
 
20
20
  // Return if value found at requested path
21
- if (current !== undefined) return current;
21
+ if (current !== undefined && current !== null) {
22
+ return current;
23
+ }
22
24
 
23
25
  // ------- Try fallback paths ----------
24
- for (const fallback of fallbacks) {
25
- if (fallback.includes(".")) {
26
- const val = getNestedValue(obj, fallback);
26
+ if (fallbacks.length === 0) return undefined;
27
27
 
28
+ const defaultVal = fallbacks[fallbacks.length - 1];
29
+ const altPaths = fallbacks.slice(0, fallbacks.length - 1);
30
+
31
+ for (const fallback of altPaths) {
32
+ if (typeof fallback === "string") {
33
+ const val = getNestedValue(obj, fallback);
28
34
  if (val !== undefined) {
29
35
  return val;
30
36
  }
31
- } else if (obj[fallback] !== undefined) {
32
- return obj[fallback];
33
37
  }
34
38
  }
35
39
 
36
- return;
40
+ return defaultVal;
37
41
  }
38
42
 
39
43
  /**
@@ -31,16 +31,42 @@ export function resolveBasePath(configValue, env = process.env) {
31
31
  }
32
32
 
33
33
  /**
34
- * Creates a function to resolve static asset paths
34
+ * Creates a function to resolve static asset paths.
35
+ * Handles portoRoot-prefixed absolute paths by extracting the bare relative
36
+ * subpath after any "assets/" segment, so they resolve correctly from
37
+ * the registered staticDirectories.
35
38
  */
36
39
  export function createStaticAssetResolver(_projectDir, staticDir, assetsDir) {
37
40
  return function (primaryPath, fallbackPath = "") {
38
- if (!primaryPath) return fallbackPath;
39
- if (/^https?:\/\//.test(primaryPath)) return primaryPath;
40
- if (fs.existsSync(path.resolve(staticDir, primaryPath))) return primaryPath;
41
- if (assetsDir && fs.existsSync(path.resolve(assetsDir, primaryPath)))
41
+ if (!primaryPath) {
42
+ return fallbackPath;
43
+ }
44
+ if (/^https?:\/\//.test(primaryPath)) {
45
+ return primaryPath;
46
+ }
47
+
48
+ // Strip known prefix segments that arise from {{portoRoot}}/src/assets/...
49
+ // or {{portoRoot}}/assets/... template paths so we get a bare relative path
50
+ // that Docusaurus can serve from its staticDirectories.
51
+ const match = primaryPath.match(/(?:src\/)?assets\/(.+)$/);
52
+ const normalizedPath = match ? match[1] : primaryPath;
53
+
54
+ if (fs.existsSync(path.resolve(staticDir, normalizedPath))) {
55
+ return normalizedPath;
56
+ }
57
+ if (assetsDir && fs.existsSync(path.resolve(assetsDir, normalizedPath))) {
58
+ return normalizedPath;
59
+ }
60
+
61
+ // Fallback: try the original path unchanged
62
+ if (fs.existsSync(path.resolve(staticDir, primaryPath))) {
63
+ return primaryPath;
64
+ }
65
+ if (assetsDir && fs.existsSync(path.resolve(assetsDir, primaryPath))) {
42
66
  return primaryPath;
43
- return fallbackPath || primaryPath;
67
+ }
68
+
69
+ return fallbackPath || normalizedPath || primaryPath;
44
70
  };
45
71
  }
46
72
 
@@ -1,19 +1,28 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
- import { fileURLToPath } from "url";
4
3
  import { logger } from "@portosaur/logger";
5
4
 
6
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
-
8
5
  /**
9
- * Extracts specific SVGs from the internal assets and saves them to the project.
6
+ * Extracts specific SVGs from a given assets directory and saves them to the project.
7
+ *
8
+ * @param {string} iconName - Icon name without the "icon-" prefix or extension.
9
+ * @param {string} destDir - Destination directory to copy the SVG into.
10
+ * @param {Object} options - Optional settings.
11
+ * @param {string} options.assetsDir - Base assets directory containing img/svg/. Required.
12
+ * @param {string} options.color - Optional fill color to inject into the SVG.
10
13
  */
11
14
  export async function extractSvg(iconName, destDir, options = {}) {
15
+ if (!options.assetsDir) {
16
+ logger.warn(
17
+ `extractSvg: assetsDir not provided, skipping icon-${iconName}.svg`,
18
+ );
19
+ return false;
20
+ }
21
+
12
22
  const srcPath = path.resolve(
13
- __dirname,
14
- `../../assets/img/svg/icon-${iconName}.svg`,
23
+ options.assetsDir,
24
+ `img/svg/icon-${iconName}.svg`,
15
25
  );
16
-
17
26
  const destPath = path.join(destDir, `icon-${iconName}.svg`);
18
27
 
19
28
  if (!fs.existsSync(srcPath)) {
@@ -28,6 +37,7 @@ export async function extractSvg(iconName, destDir, options = {}) {
28
37
  if (options.color) {
29
38
  content = content.replace(/fill="[^"]*"/g, `fill="${options.color}"`);
30
39
  }
40
+
31
41
  fs.writeFileSync(destPath, content);
32
42
 
33
43
  logger.info(`Generated SVG icon: ${destPath}`);