node-riner 1.2.3 → 1.3.1
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/index.ts +130 -96
- package/package.json +1 -1
package/index.ts
CHANGED
|
@@ -7,15 +7,13 @@ import { execSync } from 'node:child_process'
|
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Ride クラス
|
|
10
|
-
* - XML をパースしてページごとの HTML を生成し、簡易サーバで配信する機能を備える。
|
|
11
|
-
* - 実行時に XML 内の `{...}` を runs.js に書き出して node で評価する仕組みを使用している(既存の挙動を維持)。
|
|
12
10
|
*/
|
|
13
11
|
export default class Ride {
|
|
14
|
-
F: string
|
|
15
|
-
parsedJson: any
|
|
16
|
-
pages: Record<string, string>
|
|
17
|
-
name: string
|
|
18
|
-
defaultinfo: string
|
|
12
|
+
F: string
|
|
13
|
+
parsedJson: any
|
|
14
|
+
pages: Record<string, string>
|
|
15
|
+
name: string
|
|
16
|
+
defaultinfo: string
|
|
19
17
|
|
|
20
18
|
constructor(name: string) {
|
|
21
19
|
this.F = ""
|
|
@@ -26,13 +24,109 @@ export default class Ride {
|
|
|
26
24
|
}
|
|
27
25
|
|
|
28
26
|
/**
|
|
29
|
-
*
|
|
30
|
-
* -
|
|
31
|
-
* -
|
|
32
|
-
* -
|
|
33
|
-
* - `{...}` の中身は一時ファイル runs.js に書き出し node で実行、その出力で置換する(元の実装に合わせた動作)
|
|
27
|
+
* 値を再帰的に HTML に変換するユーティリティ
|
|
28
|
+
* - 文字列: `{...}` を評価して置換(既存挙動を維持)
|
|
29
|
+
* - 配列: 各要素を <item> でラップして連結
|
|
30
|
+
* - オブジェクト: 各キーをタグ名として再帰的にレンダリング
|
|
34
31
|
*
|
|
35
|
-
* 注意:
|
|
32
|
+
* 注意: '@_' で始まるキー(属性)はここではタグ化しない。
|
|
33
|
+
*/
|
|
34
|
+
private renderValue(value: any): string {
|
|
35
|
+
// null / undefined
|
|
36
|
+
if (value === null || value === undefined) return ''
|
|
37
|
+
|
|
38
|
+
// 文字列処理({...} の評価を含む)
|
|
39
|
+
if (typeof value === 'string') {
|
|
40
|
+
// 複数マッチにも対応
|
|
41
|
+
const matches = value.match(/{.*?}/g)
|
|
42
|
+
if (matches) {
|
|
43
|
+
let newValue = value
|
|
44
|
+
matches.forEach(m => {
|
|
45
|
+
const code = m.slice(1, -1) // 中身を取り出す
|
|
46
|
+
try {
|
|
47
|
+
// 既存の実装を踏襲して runs.js に書き出して node で実行
|
|
48
|
+
writeFileSync("./runs.js", code)
|
|
49
|
+
const out = execSync("node runs.js").toString()
|
|
50
|
+
newValue = newValue.replace(m, out)
|
|
51
|
+
} catch (e) {
|
|
52
|
+
newValue = newValue.replace(m, '')
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
return newValue
|
|
56
|
+
}
|
|
57
|
+
return value
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 配列: 各要素を再帰的に処理して <item> でラップ
|
|
61
|
+
if (Array.isArray(value)) {
|
|
62
|
+
return value.map((item: any) => `<item>${this.renderValue(item)}</item>`).join('')
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// オブジェクト: 属性(@_...) を無視して子要素だけタグ化する
|
|
66
|
+
if (typeof value === 'object') {
|
|
67
|
+
return Object.keys(value)
|
|
68
|
+
.filter(key => !key.startsWith('@_')) // 属性はここで無視
|
|
69
|
+
.map(key => {
|
|
70
|
+
// テキストノードは直接中身を出力
|
|
71
|
+
if (key === '#text' || key === '_text') {
|
|
72
|
+
return this.renderValue(value[key])
|
|
73
|
+
}
|
|
74
|
+
return `<${key}>${this.renderValue(value[key])}</${key}>`
|
|
75
|
+
}).join('')
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// その他(数値や真偽値など)はそのまま文字列化
|
|
79
|
+
return String(value)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* タグ名とその値から、属性を正しく反映した HTML を生成する
|
|
84
|
+
* - value が配列ならタグを複数分生成
|
|
85
|
+
* - value がオブジェクトで属性(@_...)を持つ場合、属性を開タグに組み込む
|
|
86
|
+
*/
|
|
87
|
+
private renderContentTag(tag: string, value: any): string {
|
|
88
|
+
// 配列の場合は各要素で同じタグを生成
|
|
89
|
+
if (Array.isArray(value)) {
|
|
90
|
+
return value.map((item: any) => this.renderContentTag(tag, item)).join('')
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// プリミティブ(文字列等)の場合はタグでラップ
|
|
94
|
+
if (value === null || value === undefined || typeof value !== 'object') {
|
|
95
|
+
return `<${tag}>${this.renderValue(value)}</${tag}>`
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// オブジェクトの場合、属性を抽出
|
|
99
|
+
const entries = Object.entries(value)
|
|
100
|
+
const attrEntries = entries.filter(([k]) => k.startsWith('@_'))
|
|
101
|
+
const childEntries = entries.filter(([k]) => !k.startsWith('@_'))
|
|
102
|
+
|
|
103
|
+
// 属性文字列を組み立て(@_ を取り除く)
|
|
104
|
+
const attrs = attrEntries.map(([k, v]) => {
|
|
105
|
+
const name = k.replace(/^@_/, '')
|
|
106
|
+
const safe = String(v).replace(/"/g, '"')
|
|
107
|
+
return ` ${name}="${safe}"`
|
|
108
|
+
}).join('')
|
|
109
|
+
|
|
110
|
+
// 子要素・テキストを組み立て
|
|
111
|
+
let inner = ''
|
|
112
|
+
// 優先してテキストノードを出す
|
|
113
|
+
if (value['#text'] !== undefined) {
|
|
114
|
+
inner += this.renderValue(value['#text'])
|
|
115
|
+
} else if (value['_text'] !== undefined) {
|
|
116
|
+
inner += this.renderValue(value['_text'])
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// その他の子要素をレンダリング
|
|
120
|
+
childEntries.forEach(([k, v]) => {
|
|
121
|
+
if (k === '#text' || k === '_text') return
|
|
122
|
+
inner += `<${k}>${this.renderValue(v)}</${k}>`
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
return `<${tag}${attrs}>${inner}</${tag}>`
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* XML を解析して pages に HTML を格納
|
|
36
130
|
*/
|
|
37
131
|
returnhtml(): string {
|
|
38
132
|
const options = { ignoreAttributes: false }
|
|
@@ -41,109 +135,52 @@ export default class Ride {
|
|
|
41
135
|
try {
|
|
42
136
|
this.parsedJson = parser.parse(this.F)
|
|
43
137
|
|
|
44
|
-
|
|
45
|
-
if (
|
|
46
|
-
this.parsedJson["Root"]["Page"].forEach((page: any) => {
|
|
47
|
-
const route = page['@_route']
|
|
48
|
-
|
|
49
|
-
// Content の各 tag を HTML に変換
|
|
50
|
-
const generatedhtml = Object.keys(page['Content']).map(tags => {
|
|
51
|
-
// タグの値
|
|
52
|
-
let value = page['Content'][tags]
|
|
53
|
-
|
|
54
|
-
// 文字列内に { ... } があれば評価して置換する
|
|
55
|
-
if (typeof value === "string" && typeof value.match === "function") {
|
|
56
|
-
const match = value.match(/{.*}/g)
|
|
57
|
-
if (match) {
|
|
58
|
-
let matched: string = match[0]
|
|
59
|
-
matched = matched.replace("{", "").replace("}", "")
|
|
60
|
-
// runs.js に書き出して node で実行(既存の実装を保持)
|
|
61
|
-
writeFileSync("./runs.js", matched)
|
|
62
|
-
const out = execSync("node runs.js").toString()
|
|
63
|
-
value = value.replace(/{.*}/, out)
|
|
64
|
-
// 更新しておく
|
|
65
|
-
page['Content'][tags] = value
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// オブジェクト(ネストされたタグ群) -> タグでラップして出力
|
|
70
|
-
if (typeof value === "object" && !Array.isArray(value)) {
|
|
71
|
-
const inner = Object.keys(value).map(key => {
|
|
72
|
-
return `<${key}>${value[key]}</${key}>`
|
|
73
|
-
}).join('')
|
|
74
|
-
const gene = `<${tags}>${inner}</${tags}>`
|
|
75
|
-
return gene
|
|
76
|
-
}
|
|
77
|
-
// 配列 -> item タグで列挙
|
|
78
|
-
else if (Array.isArray(value)) {
|
|
79
|
-
const gene = `<${tags}>` + value.map((item: any) => {
|
|
80
|
-
return `<item>${item}</item>`
|
|
81
|
-
}).join('') + `</${tags}>`
|
|
82
|
-
return gene
|
|
83
|
-
}
|
|
84
|
-
// 文字列等 -> 単純にタグでラップ
|
|
85
|
-
else {
|
|
86
|
-
return `<${tags}>${value}</${tags}>`
|
|
87
|
-
}
|
|
88
|
-
}).join('')
|
|
89
|
-
|
|
90
|
-
// ページを格納(タイトルを付与)
|
|
91
|
-
this.pages[route] = `<title>${this.name}</title>` + generatedhtml
|
|
92
|
-
});
|
|
93
|
-
} else {
|
|
94
|
-
// Page が単一オブジェクトの場合の処理
|
|
95
|
-
const page = this.parsedJson["Root"]["Page"]
|
|
96
|
-
const route = page['@_route']
|
|
97
|
-
|
|
98
|
-
const generatedhtml = Object.keys(page['Content']).map(tags => {
|
|
99
|
-
let value = page['Content'][tags]
|
|
100
|
-
|
|
101
|
-
if (typeof value === "string" && typeof value.match === "function") {
|
|
102
|
-
const match = value.match(/{.*}/g)
|
|
103
|
-
if (match) {
|
|
104
|
-
let matched: string = match[0]
|
|
105
|
-
matched = matched.replace("{", "").replace("}", "")
|
|
106
|
-
writeFileSync("./runs.js", matched)
|
|
107
|
-
const out = execSync("node runs.js").toString()
|
|
108
|
-
value = value.replace(/{.*}/, out)
|
|
109
|
-
page['Content'][tags] = value
|
|
110
|
-
}
|
|
111
|
-
}
|
|
138
|
+
const root = this.parsedJson?.Root
|
|
139
|
+
if (!root) return ""
|
|
112
140
|
|
|
113
|
-
|
|
141
|
+
const pages = root["Page"]
|
|
142
|
+
if (!pages) return ""
|
|
143
|
+
|
|
144
|
+
// Page が配列か単一かを吸収して配列化
|
|
145
|
+
const pageArray = Array.isArray(pages) ? pages : [pages]
|
|
146
|
+
|
|
147
|
+
pageArray.forEach((page: any) => {
|
|
148
|
+
const route = page['@_route'] || '/'
|
|
149
|
+
|
|
150
|
+
// Content が存在しない場合は空にする
|
|
151
|
+
const content = page['Content'] || {}
|
|
152
|
+
|
|
153
|
+
// 各タグを再帰的にレンダリングして結合
|
|
154
|
+
const generatedhtml = Object.keys(content).map(tag => {
|
|
155
|
+
const value = content[tag]
|
|
156
|
+
// 属性付きタグなどを正しく扱う helper を使う
|
|
157
|
+
return this.renderContentTag(tag, value)
|
|
114
158
|
}).join('')
|
|
115
159
|
|
|
116
|
-
//
|
|
160
|
+
// ページを格納(タイトルを付与)
|
|
117
161
|
this.pages[route] = `<title>${this.name}</title>` + generatedhtml
|
|
118
|
-
}
|
|
162
|
+
})
|
|
119
163
|
} catch (e) {
|
|
120
|
-
// パースエラーなどの簡易ログ
|
|
121
164
|
console.error("Whoops, an error occurred during analysis : " + e)
|
|
122
165
|
}
|
|
123
166
|
|
|
124
|
-
// 現状の戻り値は未使用のため空文字を返す(将来は生成 HTML を返す等に変更可能)
|
|
125
167
|
return ""
|
|
126
168
|
}
|
|
127
169
|
|
|
128
170
|
/**
|
|
129
|
-
* Ride メソッド(ファイル読み込みと HTML
|
|
130
|
-
* @param name ファイル名(相対パス)
|
|
171
|
+
* Ride メソッド(ファイル読み込みと HTML 生成)
|
|
131
172
|
*/
|
|
132
173
|
Ride(name: string) {
|
|
133
|
-
// __dirname から上に遡ってファイルを読んでいる既存の記述を維持
|
|
134
174
|
this.F = readFileSync(path.join(__dirname, "../../", name), { encoding: "utf-8"})
|
|
135
175
|
this.returnhtml()
|
|
136
176
|
}
|
|
137
177
|
|
|
138
178
|
/**
|
|
139
|
-
*
|
|
140
|
-
* @param port 起動ポート
|
|
141
|
-
* @returns express アプリインスタンス(テスト用途などで返す)
|
|
179
|
+
* 簡易サーバを起動
|
|
142
180
|
*/
|
|
143
181
|
serve(port: number) {
|
|
144
182
|
const app = exp();
|
|
145
183
|
|
|
146
|
-
// pages のキーをルートとして登録
|
|
147
184
|
Object.keys(this.pages).forEach(route => {
|
|
148
185
|
app.get(route, (req, res) => {
|
|
149
186
|
res.send(this.pages[route])
|
|
@@ -157,9 +194,6 @@ export default class Ride {
|
|
|
157
194
|
return app
|
|
158
195
|
}
|
|
159
196
|
|
|
160
|
-
/**
|
|
161
|
-
* 設定用(未実装)
|
|
162
|
-
*/
|
|
163
197
|
config(options: object) {
|
|
164
198
|
// 将来の拡張ポイント
|
|
165
199
|
}
|