nodoku-core 0.1.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/LICENSE +21 -0
- package/README.MD +46 -0
- package/dist/esm/bin/generate-component-resolver.js +112 -0
- package/dist/esm/bin/generate-content-schema.js +30 -0
- package/dist/esm/bin/generate-visual-schema.js +200 -0
- package/dist/esm/bin/import-load-hooks.js +30 -0
- package/dist/esm/bin/mustache/content-schema.json.mtl +92 -0
- package/dist/esm/bin/mustache/default-component-resolver.ts.mtl +16 -0
- package/dist/esm/bin/mustache/visual-schema.json.mtl +40 -0
- package/dist/esm/content/lb-content-block.js +81 -0
- package/dist/esm/content/manifest.js +16 -0
- package/dist/esm/content/providers.js +1 -0
- package/dist/esm/core/dummy-comp.jsx +69 -0
- package/dist/esm/core/rendering-page-props.js +16 -0
- package/dist/esm/core/rendering-page.jsx +166 -0
- package/dist/esm/index.js +6 -0
- package/dist/schemas/content-common-schema.json +90 -0
- package/dist/schemas/manifest-schema.json +32 -0
- package/dist/schemas/visual-common-schema.json +29 -0
- package/dist/types/bin/generate-component-resolver.d.ts +2 -0
- package/dist/types/bin/generate-content-schema.d.ts +2 -0
- package/dist/types/bin/generate-visual-schema.d.ts +2 -0
- package/dist/types/bin/import-load-hooks.d.ts +3 -0
- package/dist/types/content/lb-content-block.d.ts +60 -0
- package/dist/types/content/manifest.d.ts +11 -0
- package/dist/types/content/providers.d.ts +9 -0
- package/dist/types/core/dummy-comp.d.ts +3 -0
- package/dist/types/core/rendering-page-props.d.ts +10 -0
- package/dist/types/core/rendering-page.d.ts +4 -0
- package/dist/types/index.d.ts +8 -0
- package/package.json +49 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 nodoku
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.MD
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
nodoku-core is a foundation library for the nodoku static site generator,
|
|
2
|
+
a Node JS - React based framework for fast page prototyping using yaml files as content and page config
|
|
3
|
+
The site in Nodoku is composed of two yaml files - visual and content.
|
|
4
|
+
|
|
5
|
+
# getting started
|
|
6
|
+
|
|
7
|
+
in order to use nodoku one needs to install the nodoku-core library (this one)
|
|
8
|
+
and at least one component library for nodoku (for example, nodoku-flowbite)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
```shell
|
|
12
|
+
npm install nodoku-core nodoku-flowbite
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Then the rendering component would be as follows:
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
<RenderingPage
|
|
19
|
+
pageName={"main"}
|
|
20
|
+
lng={lng}
|
|
21
|
+
contentYamlProvider={contentYamlProvider}
|
|
22
|
+
visualYamlProvider={visualYamlProvider}
|
|
23
|
+
i18nextProvider={i18nextProvider}
|
|
24
|
+
componentProvider={defaultComponentProvider}
|
|
25
|
+
/>
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
where:
|
|
29
|
+
- **_pageName_**: the name of the page to be rendered. This name is given to the visualYamlProvider funtion to fetch the required page layout yaml file
|
|
30
|
+
|
|
31
|
+
- **_lng_**: the page language for localization. This parameter is given further to the contentYamlProvider to fetch the content on the given language
|
|
32
|
+
|
|
33
|
+
- **_contentYamlProvider_**: function providing the textual content for the page. This function is expected to fetch the content yaml file and return its content as text for the further processing. the function signature is the following: ```(lng: string, ns: string) => Promise<string>```
|
|
34
|
+
|
|
35
|
+
- **_visualYamlProvider_**: function providing the content of a yaml file that is used to render the page - the page structure yaml file. This function is expected to fetch the content yaml file and return its content as text for the further processing. the function signature is the following: ```(pageName: string) => Promise<string>```
|
|
36
|
+
|
|
37
|
+
- **_i18nextProvider_**: the page localization provider. The signature is the following: ```(lng: string) => Promise<{t: (key: string, ns: string) => string}>```. For a given language the localization provider is supposed to return a function, that would provide the value for a given key and namespace
|
|
38
|
+
|
|
39
|
+
- **_componentProvider_**: the function returning an actual implementation of the component, given its name, as specified in the page structure - visual yaml file. The signature is as follows: ```(componentName: string) => Promise<AsyncFunctionComponent>```, where AsyncFunctionComponent is the following function: ```(props: LbComponentProps) => Promise<JSX.Element>```
|
|
40
|
+
|
|
41
|
+
# generation scripts
|
|
42
|
+
to simplify the development, the nodoku-core provides the following scripts, that are used to generate component resolver, content schema and visual schema, by scanning the **node_modules** folder of the project
|
|
43
|
+
- **nodoku-gen-component-resolver**: generates the component resolver by scanning node_modules and searching for nodoku component libraries - the libraries providing the nodoku.manifest.json file
|
|
44
|
+
- **nodoku-gen-content-schema**: generates the json schema file that can be used to validate the **content** yaml file
|
|
45
|
+
- **nodoku-gen-visual-schema**: generates the json schema file thate can be used to validate the **visual** page yaml file
|
|
46
|
+
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { register } from 'node:module';
|
|
3
|
+
import { ComponentDef, Manifest } from "../content/manifest.js";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import Mustache from "mustache";
|
|
7
|
+
register('./import-load-hooks.js', import.meta.url);
|
|
8
|
+
/*
|
|
9
|
+
* we need to use a dynamic import here since otherwise we have no guarantee that the template
|
|
10
|
+
* file won't start loading prior to hooks registration
|
|
11
|
+
* we register hooks, and only then use dynamic import to load custom template file
|
|
12
|
+
*/
|
|
13
|
+
// @ts-ignore
|
|
14
|
+
const template = (await import("./mustache/default-component-resolver.ts.mtl")).default;
|
|
15
|
+
function loadComponents(dir) {
|
|
16
|
+
const comps = new Map();
|
|
17
|
+
const files = fs.readdirSync(dir);
|
|
18
|
+
console.log("reading ...", dir);
|
|
19
|
+
for (const f of files) {
|
|
20
|
+
const stat = fs.statSync(`${dir}/${f}`);
|
|
21
|
+
if (stat.isDirectory()) {
|
|
22
|
+
const m = loadComponentsByManifest(`${dir}/${f}`, f);
|
|
23
|
+
if (m) {
|
|
24
|
+
comps.set(f, m);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return comps;
|
|
29
|
+
}
|
|
30
|
+
function loadComponentsByManifest(dir, moduleName) {
|
|
31
|
+
const files = fs.readdirSync(dir);
|
|
32
|
+
for (const f of files) {
|
|
33
|
+
const stat = fs.statSync(`${dir}/${f}`);
|
|
34
|
+
if (stat.isFile()) {
|
|
35
|
+
if (f == "nodoku.manifest.json") {
|
|
36
|
+
const manifest = new Manifest(moduleName);
|
|
37
|
+
console.log("found manifest ", `${dir}/${f}`, "reading...");
|
|
38
|
+
let json;
|
|
39
|
+
if (stat.isSymbolicLink()) {
|
|
40
|
+
json = JSON.parse(fs.readlinkSync(`${dir}/nodoku.manifest.json`));
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
json = JSON.parse(fs.readFileSync(`${dir}/nodoku.manifest.json`).toString());
|
|
44
|
+
}
|
|
45
|
+
console.log("loaded manifest ", path.resolve(dir, f));
|
|
46
|
+
console.log("found manifest json ", json);
|
|
47
|
+
manifest.namespace = json.namespace;
|
|
48
|
+
Object.keys(json.components).forEach((k) => {
|
|
49
|
+
const v = json.components[k];
|
|
50
|
+
console.log("adding ", k, v);
|
|
51
|
+
// comps.set(k, Manifest.from(k, moduleName, v));
|
|
52
|
+
manifest.components.set(k, new ComponentDef(v.implementation, v.schema));
|
|
53
|
+
});
|
|
54
|
+
return manifest;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
class TemplateView {
|
|
61
|
+
modules = new Map();
|
|
62
|
+
names = new Map();
|
|
63
|
+
}
|
|
64
|
+
function calculateTemplateView(dir = undefined) {
|
|
65
|
+
if (!dir) {
|
|
66
|
+
dir = path.resolve();
|
|
67
|
+
}
|
|
68
|
+
const components = loadComponents(`${dir}/node_modules`);
|
|
69
|
+
const tv = new TemplateView();
|
|
70
|
+
components.forEach((m, k) => {
|
|
71
|
+
if (!tv.modules.get(m.moduleName)) {
|
|
72
|
+
console.log("adding ", m.moduleName);
|
|
73
|
+
tv.modules.set(m.moduleName, []);
|
|
74
|
+
}
|
|
75
|
+
if (m.namespace) {
|
|
76
|
+
tv.modules.get(m.moduleName).push(m.namespace);
|
|
77
|
+
m.components.forEach((cd, cn) => {
|
|
78
|
+
tv.names.set(cn, `${m.namespace}.${cd.implementation}`);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
m.components.forEach((cd, cn) => {
|
|
83
|
+
tv.modules.get(m.moduleName).push(cd.implementation);
|
|
84
|
+
tv.names.set(cn, cd.implementation);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
return tv;
|
|
89
|
+
}
|
|
90
|
+
export function generateComponentResolver() {
|
|
91
|
+
const args = process.argv.slice(2);
|
|
92
|
+
const dir = path.resolve();
|
|
93
|
+
const src = args[0] ? args[0] : "./src";
|
|
94
|
+
console.log("generating component resolver");
|
|
95
|
+
const tv = calculateTemplateView();
|
|
96
|
+
const view = {
|
|
97
|
+
modules: Array.from(tv.modules.entries()).map((e) => {
|
|
98
|
+
return { "module": e[0], "comps": e[1].join(", ") };
|
|
99
|
+
}),
|
|
100
|
+
names: Array.from(tv.names.entries()).map((n) => { return { n: n[0], c: n[1] }; })
|
|
101
|
+
};
|
|
102
|
+
console.log("view", JSON.stringify(view));
|
|
103
|
+
const output = Mustache.render(template, view);
|
|
104
|
+
const fileName = `${dir}/${src}/nodoku-component-resolver.ts`;
|
|
105
|
+
fs.writeFile(fileName, output, err => {
|
|
106
|
+
if (err) {
|
|
107
|
+
return console.error(err);
|
|
108
|
+
}
|
|
109
|
+
console.log(`${fileName}: File created!`);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
generateComponentResolver();
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { register } from 'node:module';
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import Mustache from "mustache";
|
|
6
|
+
register('./import-load-hooks.js', import.meta.url);
|
|
7
|
+
/*
|
|
8
|
+
* we need to use a dynamic import here since otherwise we have no guarantee that the template
|
|
9
|
+
* file won't start loading prior to hooks registration
|
|
10
|
+
* we register hooks, and only then use dynamic import to load custom template file
|
|
11
|
+
*/
|
|
12
|
+
// @ts-ignore
|
|
13
|
+
const template = (await import("./mustache/content-schema.json.mtl")).default;
|
|
14
|
+
const linkRegexPattern = "^((http|https)?:\\\\/\\\\/)?\\\\/?([-a-zA-Z0-9._\\\\+~#=]{1,256})([-a-zA-Z0-9@:%._\\\\+~#=]{1,256})([-a-zA-Z0-9()@:%_\\\\+.~#?&\\\\/\\\\/=]*)$";
|
|
15
|
+
export function generateContentSchema() {
|
|
16
|
+
const args = process.argv.slice(2);
|
|
17
|
+
const dir = path.resolve();
|
|
18
|
+
const schemaSrc = args[0] ? args[0] : "./schemas";
|
|
19
|
+
console.log("generating content schema");
|
|
20
|
+
const view = { linkRegexPattern };
|
|
21
|
+
const output = Mustache.render(template, view);
|
|
22
|
+
const fileName = `${dir}/${schemaSrc}/content-schema.json`;
|
|
23
|
+
fs.writeFile(fileName, output, err => {
|
|
24
|
+
if (err) {
|
|
25
|
+
return console.error(err);
|
|
26
|
+
}
|
|
27
|
+
console.log(`${fileName}: File created!`);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
generateContentSchema();
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { register } from 'node:module';
|
|
3
|
+
import { ComponentDef, Manifest } from "../content/manifest.js";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import Mustache from "mustache";
|
|
7
|
+
register('./import-load-hooks.js', import.meta.url);
|
|
8
|
+
/*
|
|
9
|
+
* we need to use a dynamic import here since otherwise we have no guarantee that the template
|
|
10
|
+
* file won't start loading prior to hooks registration
|
|
11
|
+
* we register hooks, and only then use dynamic import to load custom template file
|
|
12
|
+
*/
|
|
13
|
+
// @ts-ignore
|
|
14
|
+
const template = (await import("./mustache/visual-schema.json.mtl")).default;
|
|
15
|
+
function loadComponents(dir) {
|
|
16
|
+
const comps = new Map();
|
|
17
|
+
const files = fs.readdirSync(dir);
|
|
18
|
+
console.log("reading ...", dir);
|
|
19
|
+
for (const f of files) {
|
|
20
|
+
const stat = fs.statSync(`${dir}/${f}`);
|
|
21
|
+
if (stat.isDirectory()) {
|
|
22
|
+
const m = loadComponentsByManifest(`${dir}/${f}`, f);
|
|
23
|
+
if (m) {
|
|
24
|
+
comps.set(f, m);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return comps;
|
|
29
|
+
}
|
|
30
|
+
function loadComponentsByManifest(dir, moduleName) {
|
|
31
|
+
const files = fs.readdirSync(dir);
|
|
32
|
+
for (const f of files) {
|
|
33
|
+
const stat = fs.statSync(`${dir}/${f}`);
|
|
34
|
+
if (stat.isFile()) {
|
|
35
|
+
if (f == "nodoku.manifest.json") {
|
|
36
|
+
const manifest = new Manifest(moduleName);
|
|
37
|
+
console.log("found manifest ", `${dir}/${f}`, "reading...");
|
|
38
|
+
let json;
|
|
39
|
+
if (stat.isSymbolicLink()) {
|
|
40
|
+
json = JSON.parse(fs.readlinkSync(`${dir}/nodoku.manifest.json`));
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
json = JSON.parse(fs.readFileSync(`${dir}/nodoku.manifest.json`).toString());
|
|
44
|
+
}
|
|
45
|
+
console.log("loaded manifest ", path.resolve(dir, f));
|
|
46
|
+
console.log("found manifest json ", json);
|
|
47
|
+
manifest.namespace = json.namespace;
|
|
48
|
+
Object.keys(json.components).forEach((k) => {
|
|
49
|
+
const v = json.components[k];
|
|
50
|
+
console.log("adding ", k, v);
|
|
51
|
+
// comps.set(k, Manifest.from(k, moduleName, v));
|
|
52
|
+
manifest.components.set(k, new ComponentDef(v.implementation, v.schemaFile));
|
|
53
|
+
});
|
|
54
|
+
return manifest;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
class TemplateView {
|
|
61
|
+
components = new Map();
|
|
62
|
+
}
|
|
63
|
+
function calculateTemplateView(schemaDestinationDir, dirNodeModules = undefined) {
|
|
64
|
+
if (!dirNodeModules) {
|
|
65
|
+
dirNodeModules = `${path.resolve()}/node_modules`;
|
|
66
|
+
}
|
|
67
|
+
const components = loadComponents(dirNodeModules);
|
|
68
|
+
const tv = new TemplateView();
|
|
69
|
+
components.forEach((m) => {
|
|
70
|
+
m.components.forEach((cd, cn) => {
|
|
71
|
+
const moduleDir = `${dirNodeModules}/${m.moduleName}`;
|
|
72
|
+
const { content, dest } = readSchema(schemaDestinationDir, m.moduleName, moduleDir, cd.schemaFile);
|
|
73
|
+
const resolvedContent = resolveRefsInSchema(content, m.moduleName, dirNodeModules, schemaDestinationDir, cd.schemaFile);
|
|
74
|
+
const schemaFile = writeSchema(dest, resolvedContent);
|
|
75
|
+
const res = schemaFile.startsWith(schemaDestinationDir) ? schemaFile.replace(stripTrailingSlash(schemaDestinationDir), ".") : schemaFile;
|
|
76
|
+
tv.components.set(cn, res);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
const compSchemas = [];
|
|
80
|
+
let i = 0;
|
|
81
|
+
tv.components.forEach((v, k) => {
|
|
82
|
+
compSchemas.push({ name: k, schema: v, isLast: i == tv.components.size - 1 });
|
|
83
|
+
i++;
|
|
84
|
+
});
|
|
85
|
+
return { components: compSchemas };
|
|
86
|
+
}
|
|
87
|
+
function resolveRefsInSchema(schemaFileContent, moduleName, dirNodeModules, schemaDestinationDir, schemaFileRelativeToModuleDir) {
|
|
88
|
+
const regex = /\s*"\$ref": "(.*)"/;
|
|
89
|
+
const refs = [];
|
|
90
|
+
for (const line of schemaFileContent.split("\n")) {
|
|
91
|
+
const matched = regex.exec(line);
|
|
92
|
+
if (matched) {
|
|
93
|
+
let ref = matched[1];
|
|
94
|
+
if (ref.indexOf("#") >= 0) {
|
|
95
|
+
ref = ref.substring(0, ref.indexOf("#"));
|
|
96
|
+
}
|
|
97
|
+
if (ref.length > 0) {
|
|
98
|
+
refs.push(ref);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const correctedRefs = new Map();
|
|
103
|
+
refs.forEach(r => {
|
|
104
|
+
correctedRefs.set(r, resolveRef(r, moduleName, dirNodeModules, schemaDestinationDir, schemaFileRelativeToModuleDir));
|
|
105
|
+
});
|
|
106
|
+
let res = schemaFileContent;
|
|
107
|
+
correctedRefs.forEach((v, k) => {
|
|
108
|
+
console.log("replacing ref", k, "by", v);
|
|
109
|
+
res = res.replaceAll(k, v);
|
|
110
|
+
});
|
|
111
|
+
return res;
|
|
112
|
+
}
|
|
113
|
+
function resolveRef(ref, moduleName, dirNodeModules, schemaDestinationDir, schemaFileRelativeToModuleDir) {
|
|
114
|
+
const isAbsolute = path.isAbsolute(ref);
|
|
115
|
+
if (isAbsolute) {
|
|
116
|
+
return ref;
|
|
117
|
+
}
|
|
118
|
+
let relativeSchemaFileLocation = schemaFileRelativeToModuleDir;
|
|
119
|
+
if (schemaFileRelativeToModuleDir.endsWith(".json")) {
|
|
120
|
+
relativeSchemaFileLocation = schemaFileRelativeToModuleDir.substring(0, schemaFileRelativeToModuleDir.lastIndexOf("/"));
|
|
121
|
+
}
|
|
122
|
+
const fileName = `${schemaDestinationDir}/${relativeSchemaFileLocation}/${ref}`;
|
|
123
|
+
let stat = undefined;
|
|
124
|
+
try {
|
|
125
|
+
console.log("searching for ref file: ", path.resolve(fileName));
|
|
126
|
+
stat = fs.statSync(path.resolve(fileName));
|
|
127
|
+
}
|
|
128
|
+
catch (e) {
|
|
129
|
+
// file doesn't exist
|
|
130
|
+
}
|
|
131
|
+
if (stat && stat.isFile()) {
|
|
132
|
+
console.log("file is found");
|
|
133
|
+
return ref;
|
|
134
|
+
}
|
|
135
|
+
const k = ref.indexOf("node_modules");
|
|
136
|
+
if (k >= 0) {
|
|
137
|
+
const schemaFileDir = path.resolve(`${schemaDestinationDir}/${moduleName}/${relativeSchemaFileLocation}`);
|
|
138
|
+
const relativePathToNodeModules = path.relative(schemaFileDir, dirNodeModules);
|
|
139
|
+
console.log("resolving diff: schemaFileDir", schemaFileDir, "dirNodeModules", dirNodeModules, "relative path", relativePathToNodeModules);
|
|
140
|
+
ref = `${relativePathToNodeModules}/${stripLeadingSlash(ref.substring(k + "node_modules".length))}`;
|
|
141
|
+
}
|
|
142
|
+
ref = ref.replaceAll("\\", "/");
|
|
143
|
+
console.log("returning ref", ref);
|
|
144
|
+
return ref;
|
|
145
|
+
}
|
|
146
|
+
function readSchema(schemaDestinationDir, moduleName, moduleDir, compSchema) {
|
|
147
|
+
const moduleSchemaFile = `${stripTrailingSlash(moduleDir)}/${stripLeadingSlash(compSchema)}`;
|
|
148
|
+
console.log("module dir", moduleDir);
|
|
149
|
+
let destSchemaFile = `${moduleName}/${stripLeadingSlash(compSchema)}`;
|
|
150
|
+
const dest = `${schemaDestinationDir}/${destSchemaFile}`;
|
|
151
|
+
const moduleSchemaFilePath = moduleSchemaFile.replaceAll("/", "\\");
|
|
152
|
+
const stat = fs.statSync(moduleSchemaFilePath);
|
|
153
|
+
let content;
|
|
154
|
+
if (stat.isSymbolicLink()) {
|
|
155
|
+
content = fs.readlinkSync(moduleSchemaFile);
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
content = fs.readFileSync(moduleSchemaFile).toString();
|
|
159
|
+
}
|
|
160
|
+
console.log("read content of file:", moduleSchemaFilePath, ", to be copied to", dest);
|
|
161
|
+
return { content, dest };
|
|
162
|
+
}
|
|
163
|
+
function writeSchema(dest, content) {
|
|
164
|
+
console.log("dest", dest);
|
|
165
|
+
const destSchemaDir = dest.substring(0, dest.lastIndexOf("/"));
|
|
166
|
+
fs.mkdirSync(destSchemaDir, { recursive: true });
|
|
167
|
+
fs.writeFileSync(path.resolve(dest), content);
|
|
168
|
+
return dest;
|
|
169
|
+
}
|
|
170
|
+
function stripTrailingSlash(path) {
|
|
171
|
+
if (path.endsWith("/")) {
|
|
172
|
+
return path.substring(0, path.length - 1);
|
|
173
|
+
}
|
|
174
|
+
return path;
|
|
175
|
+
}
|
|
176
|
+
function stripLeadingSlash(path) {
|
|
177
|
+
if (path.startsWith("/")) {
|
|
178
|
+
return path.substring(1);
|
|
179
|
+
}
|
|
180
|
+
else if (path.startsWith("./")) {
|
|
181
|
+
return path.substring(2);
|
|
182
|
+
}
|
|
183
|
+
return path;
|
|
184
|
+
}
|
|
185
|
+
export function generateVisualSchema() {
|
|
186
|
+
const args = process.argv.slice(2);
|
|
187
|
+
const dir = path.resolve();
|
|
188
|
+
const schemaSrc = args[0] ? args[0] : "./schemas";
|
|
189
|
+
console.log("generating visual schema");
|
|
190
|
+
const view = calculateTemplateView(schemaSrc);
|
|
191
|
+
const output = Mustache.render(template, view);
|
|
192
|
+
const fileName = `${dir}/${schemaSrc}/visual-schema.json`;
|
|
193
|
+
fs.writeFile(fileName, output, err => {
|
|
194
|
+
if (err) {
|
|
195
|
+
return console.error(err);
|
|
196
|
+
}
|
|
197
|
+
console.log(`${fileName}: File created!`);
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
generateVisualSchema();
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import * as nodeUrl from "node:url";
|
|
3
|
+
import { open } from 'node:fs/promises';
|
|
4
|
+
export async function initialize(props) {
|
|
5
|
+
console.log("Receives data from `register`", props);
|
|
6
|
+
}
|
|
7
|
+
export async function resolve(specifier, context, nextResolve) {
|
|
8
|
+
console.log("Take an `import` or `require` specifier and resolve it to a URL.", specifier, context, nextResolve);
|
|
9
|
+
return nextResolve(specifier, context, nextResolve);
|
|
10
|
+
}
|
|
11
|
+
export async function load(url, context, nextLoad) {
|
|
12
|
+
console.log("Take a resolved URL and return the source code to be evaluated.", url, context, nextLoad);
|
|
13
|
+
if (url.endsWith(".mtl")) {
|
|
14
|
+
const prefix = path.resolve("./");
|
|
15
|
+
const filePath = nodeUrl.fileURLToPath(url);
|
|
16
|
+
const file = await open(filePath);
|
|
17
|
+
var templateContent = [];
|
|
18
|
+
for await (const line of file.readLines()) {
|
|
19
|
+
templateContent.push(`"${line.replaceAll("\"", "\\\"")}"`);
|
|
20
|
+
}
|
|
21
|
+
const src = `const l = ${templateContent.join(" + \"\\n\" + ")}; export default l;`;
|
|
22
|
+
const res = {
|
|
23
|
+
format: "module",
|
|
24
|
+
shortCircuit: true,
|
|
25
|
+
source: src
|
|
26
|
+
};
|
|
27
|
+
return res;
|
|
28
|
+
}
|
|
29
|
+
return nextLoad(url, context, nextLoad);
|
|
30
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
{
|
|
2
|
+
"description": "Landing buddy content page schema",
|
|
3
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
4
|
+
"definitions": {
|
|
5
|
+
"Link": {
|
|
6
|
+
"type": "string",
|
|
7
|
+
"pattern": "{{{linkRegexPattern}}}"
|
|
8
|
+
},
|
|
9
|
+
"Image": {
|
|
10
|
+
"type": "object",
|
|
11
|
+
"properties": {
|
|
12
|
+
"title": {
|
|
13
|
+
"type": "string"
|
|
14
|
+
},
|
|
15
|
+
"url": {
|
|
16
|
+
"type": "string"
|
|
17
|
+
},
|
|
18
|
+
"alt": {
|
|
19
|
+
"type": "string"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"required": [
|
|
23
|
+
"url"
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
"ContentBlock": {
|
|
27
|
+
"type": "object",
|
|
28
|
+
"properties": {
|
|
29
|
+
"title": {
|
|
30
|
+
"type": "string"
|
|
31
|
+
},
|
|
32
|
+
"subTitle": {
|
|
33
|
+
"type": "string"
|
|
34
|
+
},
|
|
35
|
+
"footer": {
|
|
36
|
+
"type": "string"
|
|
37
|
+
},
|
|
38
|
+
"images": {
|
|
39
|
+
"type": "array",
|
|
40
|
+
"items": {
|
|
41
|
+
"$ref": "#/definitions/Image"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"paragraphs": {
|
|
45
|
+
"type": "array",
|
|
46
|
+
"items": {
|
|
47
|
+
"type": "string"
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"bgImage": {
|
|
51
|
+
"$ref": "#/definitions/Image"
|
|
52
|
+
},
|
|
53
|
+
"links": {
|
|
54
|
+
"properties": {
|
|
55
|
+
"titleLink": {
|
|
56
|
+
"$ref": "#/definitions/Link"
|
|
57
|
+
},
|
|
58
|
+
"subTitleLink": {
|
|
59
|
+
"$ref": "#/definitions/Link"
|
|
60
|
+
},
|
|
61
|
+
"footerLink": {
|
|
62
|
+
"$ref": "#/definitions/Link"
|
|
63
|
+
},
|
|
64
|
+
"imageLinks": {
|
|
65
|
+
"type": "array",
|
|
66
|
+
"items": {
|
|
67
|
+
"$ref": "#/definitions/Link"
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
"paragraphLinks": {
|
|
71
|
+
"type": "array",
|
|
72
|
+
"items": {
|
|
73
|
+
"$ref": "#/definitions/Link"
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
"blockLink": {
|
|
77
|
+
"$ref": "#/definitions/Link"
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
"additionalItems": false,
|
|
81
|
+
"additionalProperties": false
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
"additionalItems": false,
|
|
85
|
+
"additionalProperties": false
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
"type": "object",
|
|
89
|
+
"additionalProperties": {
|
|
90
|
+
"$ref": "#/definitions/ContentBlock"
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import {AsyncFunctionComponent, DummyComp} from "nodoku-core";
|
|
2
|
+
|
|
3
|
+
{{#modules}}
|
|
4
|
+
import { {{comps}} } from "{{module}}";
|
|
5
|
+
{{/modules}}
|
|
6
|
+
|
|
7
|
+
const components: Map<string, AsyncFunctionComponent> = new Map<string, AsyncFunctionComponent>();
|
|
8
|
+
|
|
9
|
+
{{#names}}
|
|
10
|
+
components.set("{{{n}}}", {{c}})
|
|
11
|
+
{{/names}}
|
|
12
|
+
|
|
13
|
+
export function defaultComponentProvider(componentName: string): Promise<AsyncFunctionComponent> {
|
|
14
|
+
const f: AsyncFunctionComponent | undefined = components.get(componentName);
|
|
15
|
+
return Promise.resolve(f ? f : DummyComp);
|
|
16
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"description": "Nodoku visual page schema",
|
|
3
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
4
|
+
"definitions": {
|
|
5
|
+
"VisualRow": {
|
|
6
|
+
"type": "object",
|
|
7
|
+
"properties": {
|
|
8
|
+
"row": {
|
|
9
|
+
"type": "array",
|
|
10
|
+
"items": {
|
|
11
|
+
"$ref": "#/definitions/VisualRowBlock"
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"additionalProperties": false
|
|
16
|
+
},
|
|
17
|
+
"VisualRowBlock": {
|
|
18
|
+
"type": "object",
|
|
19
|
+
"properties": {
|
|
20
|
+
{{#components}}
|
|
21
|
+
"{{{name}}}": {
|
|
22
|
+
"$ref": "{{{schema}}}"
|
|
23
|
+
}{{^isLast}},{{/isLast}}
|
|
24
|
+
{{/components}}
|
|
25
|
+
},
|
|
26
|
+
"additionalProperties": false
|
|
27
|
+
},
|
|
28
|
+
"additionalProperties": false
|
|
29
|
+
},
|
|
30
|
+
"type": "object",
|
|
31
|
+
"properties": {
|
|
32
|
+
"rows": {
|
|
33
|
+
"type": "array",
|
|
34
|
+
"items": {
|
|
35
|
+
"$ref": "#/definitions/VisualRow"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"additionalProperties": false
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export class LbContentImage {
|
|
2
|
+
url;
|
|
3
|
+
title;
|
|
4
|
+
alt;
|
|
5
|
+
}
|
|
6
|
+
export class LbTranslatedText {
|
|
7
|
+
key = "";
|
|
8
|
+
ns = "";
|
|
9
|
+
text = "";
|
|
10
|
+
constructor(ns, key = "", text = "") {
|
|
11
|
+
this.ns = ns;
|
|
12
|
+
this.key = key;
|
|
13
|
+
this.text = text;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export class LbContentBlock {
|
|
17
|
+
key;
|
|
18
|
+
title;
|
|
19
|
+
subTitle;
|
|
20
|
+
footer;
|
|
21
|
+
paragraphs = [];
|
|
22
|
+
bgImage;
|
|
23
|
+
images = [];
|
|
24
|
+
constructor(key) {
|
|
25
|
+
this.key = key;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export class LbNsContent {
|
|
29
|
+
blocks = [];
|
|
30
|
+
}
|
|
31
|
+
export class LbContentKey {
|
|
32
|
+
key;
|
|
33
|
+
ns;
|
|
34
|
+
constructor(key, ns) {
|
|
35
|
+
this.key = key;
|
|
36
|
+
this.ns = ns;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export class LbVisualComponent {
|
|
40
|
+
rowIndex;
|
|
41
|
+
componentIndex;
|
|
42
|
+
visualComponent = "";
|
|
43
|
+
ns = "";
|
|
44
|
+
contentKeys = [];
|
|
45
|
+
theme;
|
|
46
|
+
options;
|
|
47
|
+
implementationModule = "";
|
|
48
|
+
implementationComponent = "";
|
|
49
|
+
constructor(rowIndex, componentIndex) {
|
|
50
|
+
this.rowIndex = rowIndex;
|
|
51
|
+
this.componentIndex = componentIndex;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export class LbRow {
|
|
55
|
+
rowIndex;
|
|
56
|
+
row = [];
|
|
57
|
+
constructor(rowIndex) {
|
|
58
|
+
this.rowIndex = rowIndex;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export class LbPageVisual {
|
|
62
|
+
rows = [];
|
|
63
|
+
}
|
|
64
|
+
export class LbComponentProps {
|
|
65
|
+
rowIndex;
|
|
66
|
+
componentIndex;
|
|
67
|
+
content;
|
|
68
|
+
visual;
|
|
69
|
+
options;
|
|
70
|
+
lng;
|
|
71
|
+
i18nextProvider;
|
|
72
|
+
constructor(rowIndex, componentIndex, content, visual, options, lng, i18nextProvider) {
|
|
73
|
+
this.rowIndex = rowIndex;
|
|
74
|
+
this.componentIndex = componentIndex;
|
|
75
|
+
this.content = content;
|
|
76
|
+
this.visual = visual;
|
|
77
|
+
this.options = options;
|
|
78
|
+
this.lng = lng;
|
|
79
|
+
this.i18nextProvider = i18nextProvider;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export class ComponentDef {
|
|
2
|
+
implementation;
|
|
3
|
+
schemaFile;
|
|
4
|
+
constructor(componentImplementation, componentSchema) {
|
|
5
|
+
this.implementation = componentImplementation;
|
|
6
|
+
this.schemaFile = componentSchema;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export class Manifest {
|
|
10
|
+
moduleName;
|
|
11
|
+
namespace = undefined;
|
|
12
|
+
components = new Map();
|
|
13
|
+
constructor(moduleName) {
|
|
14
|
+
this.moduleName = moduleName;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export async function DummyComp(props) {
|
|
2
|
+
console.log("content dummy comp", props.content);
|
|
3
|
+
const { content, i18nextProvider, lng } = props;
|
|
4
|
+
const { t } = await i18nextProvider(lng);
|
|
5
|
+
const blocks = content.map((block) => {
|
|
6
|
+
var style = {};
|
|
7
|
+
if (block.bgImage && block.bgImage.url) {
|
|
8
|
+
style = { backgroundImage: `url(${t(block.bgImage.url.key, block.bgImage.url.ns)})` };
|
|
9
|
+
}
|
|
10
|
+
return (<div className={"w-full w-full flex flex-col items-left justify-left border border-4 border-red-200 relative pb-10"}>
|
|
11
|
+
<div className={"top-0 bottom-0 left-0 right-0 absolute bg-cover bg-no-repeat"} style={{ ...style, zIndex: -11 }}>
|
|
12
|
+
</div>
|
|
13
|
+
<div className={"top-0 bottom-0 left-0 right-0 absolute bg-white "} style={{ zIndex: -5, opacity: 0.7 }}>
|
|
14
|
+
</div>
|
|
15
|
+
<div className={"p-5 w-full bg-red-400 text-center"}>dummy component<h3><b>{block.key}</b></h3></div>
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
<div className="p-5">
|
|
19
|
+
{block.title && <a href="#">
|
|
20
|
+
{block.title && block.title.key}
|
|
21
|
+
<h5 className={"mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white"}>
|
|
22
|
+
{block.title && t(block.title.key, block.title.ns)}
|
|
23
|
+
</h5>
|
|
24
|
+
</a>}
|
|
25
|
+
{block.subTitle && block.subTitle.key}
|
|
26
|
+
{block.subTitle && <h6 className={"mb-2 text-xl tracking-tight text-gray-900 dark:text-white"}>
|
|
27
|
+
{block.subTitle && t(block.subTitle.key, block.subTitle.ns)}
|
|
28
|
+
</h6>}
|
|
29
|
+
|
|
30
|
+
paragraphs:
|
|
31
|
+
{block.paragraphs.map((p, ip) => {
|
|
32
|
+
return (<div>
|
|
33
|
+
{p && p.key}
|
|
34
|
+
<p key={ip} className={"mb-3 font-normal text-gray-700 dark:text-gray-400"}>
|
|
35
|
+
{p && t(p.key, p.ns)}
|
|
36
|
+
</p>
|
|
37
|
+
</div>);
|
|
38
|
+
})}
|
|
39
|
+
images:
|
|
40
|
+
{block.images.map((i, ii) => {
|
|
41
|
+
return (<div>
|
|
42
|
+
<p key={"url" + ii} className={"mb-3 font-normal text-gray-700 dark:text-gray-400"}>
|
|
43
|
+
url: {i && i.url && t(i.url.key, i.url.ns)}
|
|
44
|
+
{i.url && <span className={"bg-cover bg-no-repeat"} style={{
|
|
45
|
+
display: "block",
|
|
46
|
+
width: "200px",
|
|
47
|
+
height: "200px",
|
|
48
|
+
backgroundImage: `url(${t(i.url.key, i.url.ns)})`
|
|
49
|
+
}}></span>}
|
|
50
|
+
</p>
|
|
51
|
+
<p key={"alt" + ii} className={"mb-3 font-normal text-gray-700 dark:text-gray-400"}>
|
|
52
|
+
alt: {i && i.alt && t(i.alt.key, i.alt.ns)}
|
|
53
|
+
</p>
|
|
54
|
+
<p key={"title" + ii} className={"mb-3 font-normal text-gray-700 dark:text-gray-400"}>
|
|
55
|
+
title: {i && i.title && t(i.title.key, i.title.ns)}
|
|
56
|
+
</p>
|
|
57
|
+
</div>);
|
|
58
|
+
})}
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<div className={"absolute bottom-0 p-5"}>
|
|
62
|
+
{block.footer?.key}
|
|
63
|
+
<p>{block.footer && t(block.footer.key, block.footer.ns)}</p>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
</div>);
|
|
67
|
+
});
|
|
68
|
+
return <div>{blocks}</div>;
|
|
69
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export class RenderingPageProps {
|
|
2
|
+
pageName;
|
|
3
|
+
lng;
|
|
4
|
+
i18nextProvider;
|
|
5
|
+
contentYamlProvider;
|
|
6
|
+
visualYamlProvider;
|
|
7
|
+
componentProvider;
|
|
8
|
+
constructor(pageName, lng, i18nextProvider, contentYamlProvider, visualYamlProvider, componentProvider) {
|
|
9
|
+
this.pageName = pageName;
|
|
10
|
+
this.lng = lng;
|
|
11
|
+
this.i18nextProvider = i18nextProvider;
|
|
12
|
+
this.contentYamlProvider = contentYamlProvider;
|
|
13
|
+
this.visualYamlProvider = visualYamlProvider;
|
|
14
|
+
this.componentProvider = componentProvider;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import * as yaml from "js-yaml";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { LbContentBlock, LbContentImage, LbContentKey, LbNsContent, LbRow, LbTranslatedText, LbVisualComponent } from "../content/lb-content-block";
|
|
4
|
+
import { DummyComp } from "./dummy-comp";
|
|
5
|
+
async function defaultComponentProvider(componentName) {
|
|
6
|
+
return DummyComp;
|
|
7
|
+
}
|
|
8
|
+
async function fetchPageContent(lng, namespaces, provider) {
|
|
9
|
+
const pageContent = new Map();
|
|
10
|
+
await Promise.all(namespaces.map(ns => readFileContent(lng, ns, pageContent, provider)));
|
|
11
|
+
// console.log("content", res)
|
|
12
|
+
return pageContent;
|
|
13
|
+
}
|
|
14
|
+
async function readFileContent(lng, ns, pageContent, provider) {
|
|
15
|
+
console.log("fetching file content", lng, ns);
|
|
16
|
+
await provider(lng, ns)
|
|
17
|
+
.then((fileContent) => pageContent.set(ns, fetchSuccessContent(fileContent, ns)));
|
|
18
|
+
}
|
|
19
|
+
async function fetchPageVisual(pageName, provider) {
|
|
20
|
+
const res = await provider(pageName)
|
|
21
|
+
.then(fetchSuccessVisual);
|
|
22
|
+
console.log("all visual", JSON.stringify(res));
|
|
23
|
+
return res;
|
|
24
|
+
}
|
|
25
|
+
function fetchSuccessContent(fileContents, ns) {
|
|
26
|
+
const data = yaml.load(fileContents);
|
|
27
|
+
console.log("LbPageContent", Object.keys(data));
|
|
28
|
+
const res = new LbNsContent();
|
|
29
|
+
res.blocks = Object.keys(data).map(((key, bi) => {
|
|
30
|
+
const b = data[key];
|
|
31
|
+
const blockPrefix = key;
|
|
32
|
+
const bRes = new LbContentBlock(key);
|
|
33
|
+
bRes.title = new LbTranslatedText(ns);
|
|
34
|
+
bRes.title.key = `${blockPrefix}.title`;
|
|
35
|
+
bRes.title.text = b.header;
|
|
36
|
+
bRes.subTitle = new LbTranslatedText(ns);
|
|
37
|
+
bRes.subTitle.key = `${blockPrefix}.subTitle`;
|
|
38
|
+
bRes.subTitle.text = b.subHeader;
|
|
39
|
+
bRes.footer = new LbTranslatedText(ns);
|
|
40
|
+
bRes.footer.key = `${blockPrefix}.footer`;
|
|
41
|
+
bRes.footer.text = b.footer;
|
|
42
|
+
bRes.paragraphs = b.paragraphs ? b.paragraphs.map(((p, pi) => new LbTranslatedText(ns, `${key}.paragraphs.${pi}`, p))) : [];
|
|
43
|
+
bRes.images = b.images ? b.images.map(((im, imi) => {
|
|
44
|
+
const pRes = new LbContentImage();
|
|
45
|
+
pRes.url = new LbTranslatedText(ns, `${key}.images.${imi}.url`, im.url);
|
|
46
|
+
pRes.alt = new LbTranslatedText(ns, `${key}.images.${imi}.alt`, im.alt);
|
|
47
|
+
pRes.title = new LbTranslatedText(ns, `${key}.images.${imi}.title`, im.title);
|
|
48
|
+
return pRes;
|
|
49
|
+
})) : [];
|
|
50
|
+
if (b.bgImage) {
|
|
51
|
+
bRes.bgImage = new LbContentImage();
|
|
52
|
+
bRes.bgImage.url = new LbTranslatedText(ns, `${key}.bgImage.url`, b.bgImage.url);
|
|
53
|
+
bRes.bgImage.alt = new LbTranslatedText(ns, `${key}.bgImage.alt`, b.bgImage.alt);
|
|
54
|
+
bRes.bgImage.alt = new LbTranslatedText(ns, `${key}.bgImage.title`, b.bgImage.title);
|
|
55
|
+
}
|
|
56
|
+
return bRes;
|
|
57
|
+
}));
|
|
58
|
+
return res;
|
|
59
|
+
}
|
|
60
|
+
function fetchSuccessVisual(fileContents) {
|
|
61
|
+
const data = yaml.load(fileContents);
|
|
62
|
+
return {
|
|
63
|
+
rows: data.rows.map(((r, iRow) => {
|
|
64
|
+
const row = new LbRow(iRow);
|
|
65
|
+
row.row = r.row.map((vc, iVc) => {
|
|
66
|
+
console.log("Object.keys(vc)", Object.keys(vc));
|
|
67
|
+
const vcName = Object.keys(vc)[0];
|
|
68
|
+
const lbVisualComponent = new LbVisualComponent(iRow, iVc);
|
|
69
|
+
lbVisualComponent.visualComponent = vcName;
|
|
70
|
+
const vb = vc[lbVisualComponent.visualComponent];
|
|
71
|
+
lbVisualComponent.ns = vb.ns;
|
|
72
|
+
lbVisualComponent.contentKeys = convertContentKeys(vb, lbVisualComponent.ns);
|
|
73
|
+
lbVisualComponent.theme = vb.theme;
|
|
74
|
+
lbVisualComponent.options = vb.options;
|
|
75
|
+
lbVisualComponent.implementationModule = "nodoku-flowbite";
|
|
76
|
+
lbVisualComponent.implementationComponent = lbVisualComponent.visualComponent;
|
|
77
|
+
return lbVisualComponent;
|
|
78
|
+
});
|
|
79
|
+
return row;
|
|
80
|
+
}))
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function convertContentKeys(vb, defaultNs) {
|
|
84
|
+
var contentKeys = vb.contentKeys;
|
|
85
|
+
if (!contentKeys && vb.contentKey) {
|
|
86
|
+
contentKeys = [vb.contentKey];
|
|
87
|
+
}
|
|
88
|
+
if (!contentKeys || !contentKeys.length) {
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
return contentKeys.map((ck) => {
|
|
92
|
+
if (typeof ck == "string") {
|
|
93
|
+
return new LbContentKey(ck, defaultNs);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
return new LbContentKey(ck.key, ck.ns ? ck.ns : defaultNs);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
async function RenderingPage(props) {
|
|
101
|
+
const { pageName, lng, i18nextProvider, contentYamlProvider, visualYamlProvider } = props;
|
|
102
|
+
var { componentProvider } = props;
|
|
103
|
+
if (!componentProvider) {
|
|
104
|
+
componentProvider = defaultComponentProvider;
|
|
105
|
+
}
|
|
106
|
+
const pageVisual = await fetchPageVisual(pageName, visualYamlProvider);
|
|
107
|
+
const namespaces = new Set();
|
|
108
|
+
pageVisual.rows.forEach((r) => {
|
|
109
|
+
r.row.forEach((vc) => {
|
|
110
|
+
vc.contentKeys.forEach((ck) => {
|
|
111
|
+
console.log(ck.ns);
|
|
112
|
+
namespaces.add(ck.ns);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
const pageContent = await fetchPageContent(lng, Array.from(namespaces.keys()), contentYamlProvider);
|
|
117
|
+
const l = await Promise.all(pageVisual.rows.map(async (row, iRow) => {
|
|
118
|
+
return await createSubRows(row, iRow, pageContent, lng, i18nextProvider, componentProvider);
|
|
119
|
+
}));
|
|
120
|
+
const rows = l.flatMap((a) => a);
|
|
121
|
+
return <>{rows}</>;
|
|
122
|
+
}
|
|
123
|
+
async function createSubRows(row, iRow, pageContent, lng, i18nProvider, componentProvider) {
|
|
124
|
+
const rowComponents = await Promise.all(row.row.map(async (visualSection, iComp) => {
|
|
125
|
+
return await createRowBlock(iRow, iComp, visualSection, pageContent, lng, i18nProvider, componentProvider);
|
|
126
|
+
}));
|
|
127
|
+
const numComponents = rowComponents.length;
|
|
128
|
+
if (numComponents == 1) {
|
|
129
|
+
return [rowComponents[0]];
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
const maxCols = 3;
|
|
133
|
+
const numCols = numComponents <= maxCols ? numComponents : maxCols;
|
|
134
|
+
const subRows = [];
|
|
135
|
+
for (var i = 0; i < numComponents / numCols; i++) {
|
|
136
|
+
subRows.push(<div key={`row-${iRow}`} className={`grid grid-cols-${numCols} gap-4 ${iRow == 0 ? "" : "mt-10"}`}>
|
|
137
|
+
{rowComponents.slice(numCols * i, Math.min((i + 1) * numCols, numComponents))}
|
|
138
|
+
</div>);
|
|
139
|
+
}
|
|
140
|
+
return subRows;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async function createRowBlock(rowIndex, componentIndex, visualSection, pageContent, lng, i18nProvider, componentProvider) {
|
|
144
|
+
const blocks = visualSection.contentKeys
|
|
145
|
+
.map((k) => pageContent.get(k.ns)?.blocks.find((b) => b.key == k.key))
|
|
146
|
+
.filter((b) => b != undefined)
|
|
147
|
+
.map((b) => b);
|
|
148
|
+
console.log("retrieving comp", rowIndex, componentIndex);
|
|
149
|
+
const component = await componentProvider(visualSection.implementationComponent);
|
|
150
|
+
console.log("after component");
|
|
151
|
+
return renderWithVisual(rowIndex, componentIndex, component, blocks, visualSection, lng, i18nProvider);
|
|
152
|
+
}
|
|
153
|
+
async function renderWithVisual(rowIndex, componentIndex, component, section, visualSection, lng, i18nextProvider) {
|
|
154
|
+
console.log("start rendering page");
|
|
155
|
+
const props = {
|
|
156
|
+
rowIndex: rowIndex,
|
|
157
|
+
componentIndex: componentIndex,
|
|
158
|
+
content: section,
|
|
159
|
+
visual: visualSection.theme,
|
|
160
|
+
options: visualSection.options,
|
|
161
|
+
lng: lng,
|
|
162
|
+
i18nextProvider: i18nextProvider
|
|
163
|
+
};
|
|
164
|
+
return await component(props);
|
|
165
|
+
}
|
|
166
|
+
export { RenderingPage };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { LbContentImage, LbTranslatedText, LbContentBlock, LbNsContent, LbContentKey, LbVisualComponent, LbRow, LbPageVisual, LbComponentProps } from "./content/lb-content-block";
|
|
2
|
+
import { RenderingPageProps } from "./core/rendering-page-props";
|
|
3
|
+
import { RenderingPage } from "./core/rendering-page";
|
|
4
|
+
export { LbContentImage, LbTranslatedText, LbContentBlock, LbNsContent, LbContentKey, LbVisualComponent, LbRow, LbPageVisual, LbComponentProps };
|
|
5
|
+
export { RenderingPageProps, RenderingPage };
|
|
6
|
+
export { DummyComp } from "./core/dummy-comp";
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
{
|
|
2
|
+
"description": "Nodoku content page schema",
|
|
3
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
4
|
+
"definitions": {
|
|
5
|
+
"Link": {
|
|
6
|
+
"type": "string",
|
|
7
|
+
"pattern": "^((http|https)?:\\/\\/)?\\/?([-a-zA-Z0-9._\\+~#=]{1,256})([-a-zA-Z0-9@:%._\\+~#=]{1,256})([-a-zA-Z0-9()@:%_\\+.~#?&\\/\\/=]*)$"
|
|
8
|
+
},
|
|
9
|
+
"Image": {
|
|
10
|
+
"type": "object",
|
|
11
|
+
"properties": {
|
|
12
|
+
"title": {
|
|
13
|
+
"type": "string"
|
|
14
|
+
},
|
|
15
|
+
"url": {
|
|
16
|
+
"type": "string"
|
|
17
|
+
},
|
|
18
|
+
"alt": {
|
|
19
|
+
"type": "string"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"required": [
|
|
23
|
+
"url"
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
"ContentBlock": {
|
|
27
|
+
"type": "object",
|
|
28
|
+
"properties": {
|
|
29
|
+
"title": {
|
|
30
|
+
"type": "string"
|
|
31
|
+
},
|
|
32
|
+
"subTitle": {
|
|
33
|
+
"type": "string"
|
|
34
|
+
},
|
|
35
|
+
"footer": {
|
|
36
|
+
"type": "string"
|
|
37
|
+
},
|
|
38
|
+
"images": {
|
|
39
|
+
"type": "array",
|
|
40
|
+
"items": {
|
|
41
|
+
"$ref": "#/definitions/Image"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"paragraphs": {
|
|
45
|
+
"type": "array",
|
|
46
|
+
"items": {
|
|
47
|
+
"type": "string"
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"bgImage": {
|
|
51
|
+
"$ref": "#/definitions/Image"
|
|
52
|
+
},
|
|
53
|
+
"links": {
|
|
54
|
+
"properties": {
|
|
55
|
+
"titleLink": {
|
|
56
|
+
"$ref": "#/definitions/Link"
|
|
57
|
+
},
|
|
58
|
+
"subTitleLink": {
|
|
59
|
+
"$ref": "#/definitions/Link"
|
|
60
|
+
},
|
|
61
|
+
"footerLink": {
|
|
62
|
+
"$ref": "#/definitions/Link"
|
|
63
|
+
},
|
|
64
|
+
"imageLinks": {
|
|
65
|
+
"type": "array",
|
|
66
|
+
"items": {
|
|
67
|
+
"$ref": "#/definitions/Link"
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
"paragraphLinks": {
|
|
71
|
+
"type": "array",
|
|
72
|
+
"items": {
|
|
73
|
+
"$ref": "#/definitions/Link"
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
"blockLink": {
|
|
77
|
+
"$ref": "#/definitions/Link"
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
"additionalProperties": false
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
"additionalProperties": false
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
"type": "object",
|
|
87
|
+
"additionalProperties": {
|
|
88
|
+
"$ref": "#/definitions/ContentBlock"
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"type": "object",
|
|
4
|
+
"properties": {
|
|
5
|
+
"namespace": {
|
|
6
|
+
"type": "string"
|
|
7
|
+
},
|
|
8
|
+
"components": {
|
|
9
|
+
"type": "object",
|
|
10
|
+
"additionalProperties": {
|
|
11
|
+
"type": "object",
|
|
12
|
+
"properties": {
|
|
13
|
+
"schemaFile": {
|
|
14
|
+
"type": "string"
|
|
15
|
+
},
|
|
16
|
+
"implementation": {
|
|
17
|
+
"type": "string"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"required": [
|
|
21
|
+
"schemaFile",
|
|
22
|
+
"implementation"
|
|
23
|
+
],
|
|
24
|
+
"additionalProperties": false
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"required": [
|
|
29
|
+
"components"
|
|
30
|
+
],
|
|
31
|
+
"additionalProperties": false
|
|
32
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"definitions": {
|
|
4
|
+
"ContentKey": {
|
|
5
|
+
"oneOf": [
|
|
6
|
+
{
|
|
7
|
+
"type": "object",
|
|
8
|
+
"properties": {
|
|
9
|
+
"ns": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"pattern": "[\\w-]+"
|
|
12
|
+
},
|
|
13
|
+
"key": {
|
|
14
|
+
"type": "string",
|
|
15
|
+
"pattern": "[\\w-]+"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"required": [
|
|
19
|
+
"key"
|
|
20
|
+
],
|
|
21
|
+
"additionalProperties": false
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"type": "string"
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { i18nextProvider } from "./providers";
|
|
2
|
+
export declare class LbContentImage {
|
|
3
|
+
url?: LbTranslatedText;
|
|
4
|
+
title?: LbTranslatedText;
|
|
5
|
+
alt?: LbTranslatedText;
|
|
6
|
+
}
|
|
7
|
+
export declare class LbTranslatedText {
|
|
8
|
+
key: string;
|
|
9
|
+
ns: string;
|
|
10
|
+
text: string;
|
|
11
|
+
constructor(ns: string, key?: string, text?: string);
|
|
12
|
+
}
|
|
13
|
+
export declare class LbContentBlock {
|
|
14
|
+
key: string;
|
|
15
|
+
title?: LbTranslatedText;
|
|
16
|
+
subTitle?: LbTranslatedText;
|
|
17
|
+
footer?: LbTranslatedText;
|
|
18
|
+
paragraphs: LbTranslatedText[];
|
|
19
|
+
bgImage?: LbContentImage;
|
|
20
|
+
images: LbContentImage[];
|
|
21
|
+
constructor(key: string);
|
|
22
|
+
}
|
|
23
|
+
export declare class LbNsContent {
|
|
24
|
+
blocks: LbContentBlock[];
|
|
25
|
+
}
|
|
26
|
+
export declare class LbContentKey {
|
|
27
|
+
key: string;
|
|
28
|
+
ns: string;
|
|
29
|
+
constructor(key: string, ns: string);
|
|
30
|
+
}
|
|
31
|
+
export declare class LbVisualComponent {
|
|
32
|
+
rowIndex: number;
|
|
33
|
+
componentIndex: number;
|
|
34
|
+
visualComponent: string;
|
|
35
|
+
ns: string;
|
|
36
|
+
contentKeys: LbContentKey[];
|
|
37
|
+
theme: any;
|
|
38
|
+
options: any;
|
|
39
|
+
implementationModule: string;
|
|
40
|
+
implementationComponent: string;
|
|
41
|
+
constructor(rowIndex: number, componentIndex: number);
|
|
42
|
+
}
|
|
43
|
+
export declare class LbRow {
|
|
44
|
+
rowIndex: number;
|
|
45
|
+
row: LbVisualComponent[];
|
|
46
|
+
constructor(rowIndex: number);
|
|
47
|
+
}
|
|
48
|
+
export declare class LbPageVisual {
|
|
49
|
+
rows: LbRow[];
|
|
50
|
+
}
|
|
51
|
+
export declare class LbComponentProps<TComponentTheme = any, TComponentOptions = any> {
|
|
52
|
+
rowIndex: number;
|
|
53
|
+
componentIndex: number;
|
|
54
|
+
content: LbContentBlock[];
|
|
55
|
+
visual: TComponentTheme;
|
|
56
|
+
options: TComponentOptions;
|
|
57
|
+
lng: string;
|
|
58
|
+
i18nextProvider: i18nextProvider;
|
|
59
|
+
constructor(rowIndex: number, componentIndex: number, content: LbContentBlock[], visual: TComponentTheme, options: TComponentOptions, lng: string, i18nextProvider: i18nextProvider);
|
|
60
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare class ComponentDef {
|
|
2
|
+
implementation: string;
|
|
3
|
+
schemaFile: string;
|
|
4
|
+
constructor(componentImplementation: string, componentSchema: string);
|
|
5
|
+
}
|
|
6
|
+
export declare class Manifest {
|
|
7
|
+
moduleName: string;
|
|
8
|
+
namespace: string | undefined;
|
|
9
|
+
components: Map<string, ComponentDef>;
|
|
10
|
+
constructor(moduleName: string);
|
|
11
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { LbComponentProps } from "./lb-content-block";
|
|
2
|
+
import { JSX } from "react";
|
|
3
|
+
export type AsyncFunctionComponent = (props: LbComponentProps) => Promise<JSX.Element>;
|
|
4
|
+
export type ContentYamlProvider = (lng: string, ns: string) => Promise<string>;
|
|
5
|
+
export type VisualYamlProvider = (pageName: string) => Promise<string>;
|
|
6
|
+
export type i18nextProvider = (lng: string) => Promise<{
|
|
7
|
+
t: (key: string, ns: string) => string;
|
|
8
|
+
}>;
|
|
9
|
+
export type ComponentProvider = (componentName: string) => Promise<AsyncFunctionComponent>;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ComponentProvider, ContentYamlProvider, i18nextProvider, VisualYamlProvider } from "../content/providers";
|
|
2
|
+
export declare class RenderingPageProps {
|
|
3
|
+
pageName: string;
|
|
4
|
+
lng: string;
|
|
5
|
+
i18nextProvider: i18nextProvider;
|
|
6
|
+
contentYamlProvider: ContentYamlProvider;
|
|
7
|
+
visualYamlProvider: VisualYamlProvider;
|
|
8
|
+
componentProvider: ComponentProvider;
|
|
9
|
+
constructor(pageName: string, lng: string, i18nextProvider: i18nextProvider, contentYamlProvider: ContentYamlProvider, visualYamlProvider: VisualYamlProvider, componentProvider: ComponentProvider);
|
|
10
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { LbContentImage, LbTranslatedText, LbContentBlock, LbNsContent, LbContentKey, LbVisualComponent, LbRow, LbPageVisual, LbComponentProps } from "./content/lb-content-block";
|
|
2
|
+
import { RenderingPageProps } from "./core/rendering-page-props";
|
|
3
|
+
import { RenderingPage } from "./core/rendering-page";
|
|
4
|
+
import { ContentYamlProvider, VisualYamlProvider, i18nextProvider, AsyncFunctionComponent } from "./content/providers";
|
|
5
|
+
export { LbContentImage, LbTranslatedText, LbContentBlock, LbNsContent, LbContentKey, LbVisualComponent, LbRow, LbPageVisual, LbComponentProps };
|
|
6
|
+
export { RenderingPageProps, RenderingPage };
|
|
7
|
+
export type { ContentYamlProvider, VisualYamlProvider, i18nextProvider, AsyncFunctionComponent };
|
|
8
|
+
export { DummyComp } from "./core/dummy-comp";
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nodoku-core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "basic foundation for nodoku static site generator",
|
|
5
|
+
"exports": {
|
|
6
|
+
".": {
|
|
7
|
+
"types": "./dist/types/index.d.ts",
|
|
8
|
+
"import": "./dist/esm/index.js"
|
|
9
|
+
},
|
|
10
|
+
"./package.json": "./package.json"
|
|
11
|
+
},
|
|
12
|
+
"module": "dist/esm/index.js",
|
|
13
|
+
"types": "dist/types/index.d.ts",
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"type": "module",
|
|
18
|
+
"bin": {
|
|
19
|
+
"nodoku-gen-component-resolver": "./dist/esm/bin/generate-component-resolver.js",
|
|
20
|
+
"nodoku-gen-content-schema": "./dist/esm/bin/generate-content-schema.js",
|
|
21
|
+
"nodoku-gen-visual-schema": "./dist/esm/bin/generate-visual-schema.js"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@types/js-yaml": "^4.0.9",
|
|
25
|
+
"@types/mustache": "^4.2.5",
|
|
26
|
+
"js-yaml": "^4.1.0",
|
|
27
|
+
"mustache": "^4.2.0"
|
|
28
|
+
},
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"@types/react": "^18.3.2",
|
|
31
|
+
"@types/react-dom": "18.3.0",
|
|
32
|
+
"react": "^18.3.1",
|
|
33
|
+
"react-dom": "^18.3.1"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/node": "^20.12.11",
|
|
37
|
+
"@types/react": "^18.3.3",
|
|
38
|
+
"@types/react-dom": "^18.3.0",
|
|
39
|
+
"eslint": "^8.56.0",
|
|
40
|
+
"eslint-config-next": "14.2.3",
|
|
41
|
+
"react": "^18.3.1",
|
|
42
|
+
"react-dom": "^18.3.1",
|
|
43
|
+
"typescript": "^5.5.3",
|
|
44
|
+
"shx": "^0.3.4"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"dist": "tsc && shx cp -r src/bin/mustache dist/esm/bin/ && shx cp -r schemas dist"
|
|
48
|
+
}
|
|
49
|
+
}
|