@sorane/okf 0.2.6 → 0.2.7
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 +1 -1
- package/profile/sorane-okf-0.3.schema.json +171 -0
- package/src/index.ts +11 -0
- package/src/profile.ts +62 -0
- package/src/validate.ts +51 -22
package/package.json
CHANGED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://sorane.dev/profile/sorane-okf/0.3",
|
|
4
|
+
"title": "sorane-okf/0.3",
|
|
5
|
+
"description": "OKF v0.1 profile for sorane SSG: extended types, open-data metadata, AI disclosure.",
|
|
6
|
+
"oneOf": [
|
|
7
|
+
{ "$ref": "#/$defs/article" },
|
|
8
|
+
{ "$ref": "#/$defs/index" },
|
|
9
|
+
{ "$ref": "#/$defs/dataset" },
|
|
10
|
+
{ "$ref": "#/$defs/reference" },
|
|
11
|
+
{ "$ref": "#/$defs/glossary" },
|
|
12
|
+
{ "$ref": "#/$defs/faq" }
|
|
13
|
+
],
|
|
14
|
+
"$defs": {
|
|
15
|
+
"publisher": {
|
|
16
|
+
"type": "object",
|
|
17
|
+
"required": ["name"],
|
|
18
|
+
"properties": {
|
|
19
|
+
"name": { "type": "string", "minLength": 1 },
|
|
20
|
+
"url": { "type": "string", "minLength": 1 },
|
|
21
|
+
"email": { "type": "string", "minLength": 1 }
|
|
22
|
+
},
|
|
23
|
+
"additionalProperties": false
|
|
24
|
+
},
|
|
25
|
+
"distribution": {
|
|
26
|
+
"type": "object",
|
|
27
|
+
"required": ["title", "format", "accessURL"],
|
|
28
|
+
"properties": {
|
|
29
|
+
"title": { "type": "string", "minLength": 1 },
|
|
30
|
+
"format": { "type": "string", "minLength": 1 },
|
|
31
|
+
"accessURL": { "type": "string", "minLength": 1 },
|
|
32
|
+
"downloadURL": { "type": "string", "minLength": 1 },
|
|
33
|
+
"byteSize": { "type": "integer", "minimum": 0 },
|
|
34
|
+
"checksum": { "type": "string", "minLength": 1 }
|
|
35
|
+
},
|
|
36
|
+
"additionalProperties": false
|
|
37
|
+
},
|
|
38
|
+
"temporal": {
|
|
39
|
+
"type": "object",
|
|
40
|
+
"properties": {
|
|
41
|
+
"start": { "type": "string", "minLength": 1 },
|
|
42
|
+
"end": { "type": "string", "minLength": 1 }
|
|
43
|
+
},
|
|
44
|
+
"additionalProperties": false
|
|
45
|
+
},
|
|
46
|
+
"okfBase": {
|
|
47
|
+
"type": "object",
|
|
48
|
+
"required": ["type"],
|
|
49
|
+
"properties": {
|
|
50
|
+
"type": { "type": "string", "minLength": 1 },
|
|
51
|
+
"title": { "type": "string", "minLength": 1 },
|
|
52
|
+
"description": { "type": "string" },
|
|
53
|
+
"resource": { "type": "string" },
|
|
54
|
+
"tags": {
|
|
55
|
+
"type": "array",
|
|
56
|
+
"items": { "type": "string" }
|
|
57
|
+
},
|
|
58
|
+
"timestamp": { "type": "string", "format": "date-time" },
|
|
59
|
+
"profile": { "type": "string" },
|
|
60
|
+
"identifier": { "type": "string", "minLength": 1 },
|
|
61
|
+
"language": { "type": "string", "minLength": 2 },
|
|
62
|
+
"license": { "type": "string", "minLength": 1 },
|
|
63
|
+
"publisher": { "$ref": "#/$defs/publisher" },
|
|
64
|
+
"theme": { "type": "string", "minLength": 1 },
|
|
65
|
+
"temporal": { "$ref": "#/$defs/temporal" },
|
|
66
|
+
"spatial": { "type": "string", "minLength": 1 },
|
|
67
|
+
"distributions": {
|
|
68
|
+
"type": "array",
|
|
69
|
+
"items": { "$ref": "#/$defs/distribution" }
|
|
70
|
+
},
|
|
71
|
+
"creativeWorkType": {
|
|
72
|
+
"type": "string",
|
|
73
|
+
"enum": ["BlogPosting", "TechArticle"]
|
|
74
|
+
},
|
|
75
|
+
"digitalSourceType": {
|
|
76
|
+
"type": "string",
|
|
77
|
+
"minLength": 1
|
|
78
|
+
},
|
|
79
|
+
"euAiLabel": {
|
|
80
|
+
"type": "string",
|
|
81
|
+
"enum": ["basic", "fully-generated", "partially-modified"]
|
|
82
|
+
},
|
|
83
|
+
"aiDisclosureNote": { "type": "string", "maxLength": 500 },
|
|
84
|
+
"aiSystems": {
|
|
85
|
+
"type": "array",
|
|
86
|
+
"maxItems": 10,
|
|
87
|
+
"items": {
|
|
88
|
+
"type": "object",
|
|
89
|
+
"required": ["name"],
|
|
90
|
+
"properties": {
|
|
91
|
+
"name": { "type": "string", "minLength": 1 },
|
|
92
|
+
"version": { "type": "string" },
|
|
93
|
+
"provider": { "type": "string" }
|
|
94
|
+
},
|
|
95
|
+
"additionalProperties": false
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
"additionalProperties": true
|
|
100
|
+
},
|
|
101
|
+
"article": {
|
|
102
|
+
"allOf": [
|
|
103
|
+
{ "$ref": "#/$defs/okfBase" },
|
|
104
|
+
{
|
|
105
|
+
"properties": { "type": { "const": "article" } },
|
|
106
|
+
"required": ["type", "title"]
|
|
107
|
+
}
|
|
108
|
+
]
|
|
109
|
+
},
|
|
110
|
+
"index": {
|
|
111
|
+
"allOf": [
|
|
112
|
+
{ "$ref": "#/$defs/okfBase" },
|
|
113
|
+
{
|
|
114
|
+
"properties": { "type": { "const": "index" } },
|
|
115
|
+
"required": ["type", "title"]
|
|
116
|
+
}
|
|
117
|
+
]
|
|
118
|
+
},
|
|
119
|
+
"dataset": {
|
|
120
|
+
"allOf": [
|
|
121
|
+
{ "$ref": "#/$defs/okfBase" },
|
|
122
|
+
{
|
|
123
|
+
"properties": {
|
|
124
|
+
"type": { "const": "dataset" },
|
|
125
|
+
"distributions": {
|
|
126
|
+
"type": "array",
|
|
127
|
+
"minItems": 1,
|
|
128
|
+
"items": { "$ref": "#/$defs/distribution" }
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
"required": [
|
|
132
|
+
"type",
|
|
133
|
+
"title",
|
|
134
|
+
"description",
|
|
135
|
+
"resource",
|
|
136
|
+
"license",
|
|
137
|
+
"publisher",
|
|
138
|
+
"distributions"
|
|
139
|
+
]
|
|
140
|
+
}
|
|
141
|
+
]
|
|
142
|
+
},
|
|
143
|
+
"reference": {
|
|
144
|
+
"allOf": [
|
|
145
|
+
{ "$ref": "#/$defs/okfBase" },
|
|
146
|
+
{
|
|
147
|
+
"properties": { "type": { "const": "reference" } },
|
|
148
|
+
"required": ["type", "title"]
|
|
149
|
+
}
|
|
150
|
+
]
|
|
151
|
+
},
|
|
152
|
+
"glossary": {
|
|
153
|
+
"allOf": [
|
|
154
|
+
{ "$ref": "#/$defs/okfBase" },
|
|
155
|
+
{
|
|
156
|
+
"properties": { "type": { "const": "glossary" } },
|
|
157
|
+
"required": ["type", "title"]
|
|
158
|
+
}
|
|
159
|
+
]
|
|
160
|
+
},
|
|
161
|
+
"faq": {
|
|
162
|
+
"allOf": [
|
|
163
|
+
{ "$ref": "#/$defs/okfBase" },
|
|
164
|
+
{
|
|
165
|
+
"properties": { "type": { "const": "faq" } },
|
|
166
|
+
"required": ["type", "title"]
|
|
167
|
+
}
|
|
168
|
+
]
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -12,6 +12,17 @@ export {
|
|
|
12
12
|
type ValidationResult,
|
|
13
13
|
type ValidationIssue,
|
|
14
14
|
} from "./validate.ts";
|
|
15
|
+
export {
|
|
16
|
+
DEFAULT_PROFILE,
|
|
17
|
+
SUPPORTED_PROFILE_RE,
|
|
18
|
+
TYPES_03,
|
|
19
|
+
TYPES_01_02,
|
|
20
|
+
BUILDABLE_CONTENT_TYPES,
|
|
21
|
+
isProfile03,
|
|
22
|
+
resolveEffectiveType,
|
|
23
|
+
isBuildableContentType,
|
|
24
|
+
resolveProfileForValidation,
|
|
25
|
+
} from "./profile.ts";
|
|
15
26
|
export {
|
|
16
27
|
IPTC_BASE,
|
|
17
28
|
resolveDigitalSourceType,
|
package/src/profile.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export const SUPPORTED_PROFILE_RE = /^sorane-okf\/(0\.[123])$/;
|
|
2
|
+
export const DEFAULT_PROFILE = "sorane-okf/0.1";
|
|
3
|
+
|
|
4
|
+
export const TYPES_01_02 = new Set(["article", "index"]);
|
|
5
|
+
|
|
6
|
+
export const TYPES_03 = new Set([
|
|
7
|
+
"article",
|
|
8
|
+
"index",
|
|
9
|
+
"dataset",
|
|
10
|
+
"reference",
|
|
11
|
+
"glossary",
|
|
12
|
+
"faq",
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
export const BUILDABLE_CONTENT_TYPES = new Set([
|
|
16
|
+
"article",
|
|
17
|
+
"dataset",
|
|
18
|
+
"reference",
|
|
19
|
+
"glossary",
|
|
20
|
+
"faq",
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
export function profileMajor(profile: string | undefined): "0.1" | "0.2" | "0.3" | null {
|
|
24
|
+
if (profile === undefined) return null;
|
|
25
|
+
const m = profile.match(/^sorane-okf\/(0\.[123])$/);
|
|
26
|
+
if (!m) return null;
|
|
27
|
+
return m[1] as "0.1" | "0.2" | "0.3";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function isProfile03(profile: string | undefined): boolean {
|
|
31
|
+
return profile === "sorane-okf/0.3";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function resolveProfileForValidation(
|
|
35
|
+
profile: string | undefined,
|
|
36
|
+
): string {
|
|
37
|
+
if (profile !== undefined && SUPPORTED_PROFILE_RE.test(profile)) {
|
|
38
|
+
return profile;
|
|
39
|
+
}
|
|
40
|
+
return DEFAULT_PROFILE;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Build/validate effective type (0.3 unknown types → article). */
|
|
44
|
+
export function resolveEffectiveType(
|
|
45
|
+
type: string,
|
|
46
|
+
profile: string | undefined,
|
|
47
|
+
): string {
|
|
48
|
+
if (!type) return "";
|
|
49
|
+
if (isProfile03(profile)) {
|
|
50
|
+
if (TYPES_03.has(type)) return type;
|
|
51
|
+
return "article";
|
|
52
|
+
}
|
|
53
|
+
return type;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function isBuildableContentType(
|
|
57
|
+
type: string,
|
|
58
|
+
profile: string | undefined,
|
|
59
|
+
): boolean {
|
|
60
|
+
const effective = resolveEffectiveType(type, profile);
|
|
61
|
+
return BUILDABLE_CONTENT_TYPES.has(effective);
|
|
62
|
+
}
|
package/src/validate.ts
CHANGED
|
@@ -6,16 +6,19 @@ import { fileURLToPath } from "node:url";
|
|
|
6
6
|
import { validateDisclosureFields } from "./digital-source-type.ts";
|
|
7
7
|
import { extract } from "./extract.ts";
|
|
8
8
|
import { normalizeConcept } from "./normalize.ts";
|
|
9
|
+
import {
|
|
10
|
+
DEFAULT_PROFILE,
|
|
11
|
+
isProfile03,
|
|
12
|
+
resolveProfileForValidation,
|
|
13
|
+
SUPPORTED_PROFILE_RE,
|
|
14
|
+
TYPES_01_02,
|
|
15
|
+
TYPES_03,
|
|
16
|
+
} from "./profile.ts";
|
|
9
17
|
import { parseYaml } from "./yaml.ts";
|
|
10
18
|
|
|
11
19
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
20
|
const PROFILE_SCHEMA_DIR = join(__dirname, "../profile");
|
|
13
21
|
|
|
14
|
-
const SUPPORTED_PROFILE_RE = /^sorane-okf\/(0\.[12])$/;
|
|
15
|
-
const DEFAULT_PROFILE = "sorane-okf/0.1";
|
|
16
|
-
|
|
17
|
-
const SUPPORTED_TYPES = new Set(["article", "index"]);
|
|
18
|
-
|
|
19
22
|
export interface ValidationIssue {
|
|
20
23
|
readonly where: "structure" | "frontmatter" | "type" | "profile";
|
|
21
24
|
readonly message: string;
|
|
@@ -39,7 +42,7 @@ export function validateProfileFormat(
|
|
|
39
42
|
if (!SUPPORTED_PROFILE_RE.test(profile)) {
|
|
40
43
|
return {
|
|
41
44
|
where: "profile",
|
|
42
|
-
message: `Unsupported profile "${profile}"; supported: sorane-okf/0.1, sorane-okf/0.2`,
|
|
45
|
+
message: `Unsupported profile "${profile}"; supported: sorane-okf/0.1, sorane-okf/0.2, sorane-okf/0.3`,
|
|
43
46
|
};
|
|
44
47
|
}
|
|
45
48
|
return null;
|
|
@@ -70,6 +73,37 @@ function slugFromPath(filePath: string): string {
|
|
|
70
73
|
return base.replace(/\.md$/i, "");
|
|
71
74
|
}
|
|
72
75
|
|
|
76
|
+
function validateTypeForProfile(
|
|
77
|
+
type: string,
|
|
78
|
+
profile: string,
|
|
79
|
+
issues: ValidationIssue[],
|
|
80
|
+
warnings: string[],
|
|
81
|
+
): void {
|
|
82
|
+
if (!type) {
|
|
83
|
+
issues.push({
|
|
84
|
+
where: "type",
|
|
85
|
+
message: "OKF 必須フィールド `type` がありません",
|
|
86
|
+
});
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (isProfile03(profile)) {
|
|
91
|
+
if (!TYPES_03.has(type)) {
|
|
92
|
+
warnings.push(
|
|
93
|
+
`unknown type "${type}" (sorane-okf/0.3); build treats as article`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!TYPES_01_02.has(type)) {
|
|
100
|
+
issues.push({
|
|
101
|
+
where: "type",
|
|
102
|
+
message: `未サポートの type: ${type}(${profile} は article / index のみ)`,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
73
107
|
/** 1 つの markdown ソースを sorane-okf プロファイルで検証する。 */
|
|
74
108
|
export function validateSource(file: string, source: string): ValidationResult {
|
|
75
109
|
const { frontmatter, body } = extract(source);
|
|
@@ -126,29 +160,24 @@ export function validateSource(file: string, source: string): ValidationResult {
|
|
|
126
160
|
const concept = normalizeConcept(raw, body, slugFromPath(file));
|
|
127
161
|
warnings.push(...concept.warnings);
|
|
128
162
|
|
|
129
|
-
|
|
130
|
-
issues.push({
|
|
131
|
-
where: "type",
|
|
132
|
-
message: "OKF 必須フィールド `type` がありません",
|
|
133
|
-
});
|
|
134
|
-
} else if (!SUPPORTED_TYPES.has(concept.type)) {
|
|
135
|
-
issues.push({
|
|
136
|
-
where: "type",
|
|
137
|
-
message: `未サポートの type: ${concept.type}(v0.1 は article / index のみ)`,
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const profile =
|
|
163
|
+
const profile = resolveProfileForValidation(
|
|
142
164
|
typeof concept.profile === "string" && SUPPORTED_PROFILE_RE.test(concept.profile)
|
|
143
165
|
? concept.profile
|
|
144
|
-
:
|
|
166
|
+
: undefined,
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
validateTypeForProfile(concept.type, profile, issues, warnings);
|
|
145
170
|
|
|
146
171
|
if (issues.every((i) => i.where !== "profile")) {
|
|
147
172
|
const validate = getValidatorForProfile(profile);
|
|
173
|
+
const schemaType =
|
|
174
|
+
isProfile03(profile) && concept.type && !TYPES_03.has(concept.type)
|
|
175
|
+
? "article"
|
|
176
|
+
: concept.type || "article";
|
|
148
177
|
const fmForSchema: Record<string, unknown> = {
|
|
149
|
-
type: concept.type || "article",
|
|
150
178
|
title: concept.title,
|
|
151
179
|
...concept.frontmatter,
|
|
180
|
+
type: schemaType,
|
|
152
181
|
};
|
|
153
182
|
if (concept.timestamp) fmForSchema.timestamp = concept.timestamp;
|
|
154
183
|
if (concept.description) fmForSchema.description = concept.description;
|
|
@@ -167,7 +196,7 @@ export function validateSource(file: string, source: string): ValidationResult {
|
|
|
167
196
|
}
|
|
168
197
|
}
|
|
169
198
|
|
|
170
|
-
const strictDisclosure = profile === "sorane-okf/0.2";
|
|
199
|
+
const strictDisclosure = profile === "sorane-okf/0.2" || profile === "sorane-okf/0.3";
|
|
171
200
|
const disclosure = validateDisclosureFields(concept.frontmatter, strictDisclosure);
|
|
172
201
|
warnings.push(...disclosure.warnings);
|
|
173
202
|
for (const d of disclosure.issues) {
|