node-riner 1.3.1 → 1.3.2
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 +74 -52
- package/package.json +1 -1
package/index.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
// ...existing code...
|
|
2
1
|
import fs, { readFileSync, writeFileSync } from 'fs'
|
|
3
2
|
import { XMLParser } from 'fast-xml-parser'
|
|
4
3
|
import exp from 'express'
|
|
5
4
|
import path from 'path'
|
|
6
5
|
import { execSync } from 'node:child_process'
|
|
6
|
+
import { runInNewContext } from 'vm'
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Ride クラス
|
|
@@ -12,45 +12,58 @@ export default class Ride {
|
|
|
12
12
|
F: string
|
|
13
13
|
parsedJson: any
|
|
14
14
|
pages: Record<string, string>
|
|
15
|
+
pagesTemplates: Record<string, any> // ルート => 生の Content オブジェクト(リクエスト毎に評価)
|
|
15
16
|
name: string
|
|
16
17
|
defaultinfo: string
|
|
18
|
+
defaultContext: any // サーバ/アプリ側で渡したい既定のコンテキスト
|
|
17
19
|
|
|
18
20
|
constructor(name: string) {
|
|
19
21
|
this.F = ""
|
|
20
22
|
this.parsedJson = {}
|
|
21
23
|
this.pages = {}
|
|
24
|
+
this.pagesTemplates = {}
|
|
22
25
|
this.name = name
|
|
23
26
|
this.defaultinfo = ""
|
|
27
|
+
this.defaultContext = {}
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
/**
|
|
27
31
|
* 値を再帰的に HTML に変換するユーティリティ
|
|
28
|
-
* -
|
|
29
|
-
* -
|
|
30
|
-
* - オブジェクト: 各キーをタグ名として再帰的にレンダリング
|
|
31
|
-
*
|
|
32
|
-
* 注意: '@_' で始まるキー(属性)はここではタグ化しない。
|
|
32
|
+
* - ctx を渡すと { ... } 内のコードをそのコンテキストで評価する
|
|
33
|
+
* - 出力は console.log() の内容を優先して取得し、なければ評価結果を文字列化する
|
|
33
34
|
*/
|
|
34
|
-
private renderValue(value: any): string {
|
|
35
|
-
// null / undefined
|
|
35
|
+
private renderValue(value: any, ctx: any = {}): string {
|
|
36
36
|
if (value === null || value === undefined) return ''
|
|
37
37
|
|
|
38
38
|
// 文字列処理({...} の評価を含む)
|
|
39
39
|
if (typeof value === 'string') {
|
|
40
|
-
// 複数マッチにも対応
|
|
41
40
|
const matches = value.match(/{.*?}/g)
|
|
42
41
|
if (matches) {
|
|
43
42
|
let newValue = value
|
|
44
43
|
matches.forEach(m => {
|
|
45
|
-
const code = m.slice(1, -1) //
|
|
44
|
+
const code = m.slice(1, -1) // 中身
|
|
45
|
+
// sandbox を作り、console.log の出力を収集する
|
|
46
|
+
const output: string[] = []
|
|
47
|
+
const sandbox = Object.assign({}, ctx, {
|
|
48
|
+
console: {
|
|
49
|
+
log: (...args: any[]) => output.push(args.join(' ')),
|
|
50
|
+
error: (...args: any[]) => output.push(args.join(' ')),
|
|
51
|
+
warn: (...args: any[]) => output.push(args.join(' '))
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
// まずは vm で実行(タイムアウト付き)
|
|
56
|
+
let res: any
|
|
46
57
|
try {
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
const out = execSync("node runs.js").toString()
|
|
50
|
-
newValue = newValue.replace(m, out)
|
|
58
|
+
// 実行が式でも文でも動くようにそのまま実行
|
|
59
|
+
res = runInNewContext(code, sandbox, { timeout: 2000 })
|
|
51
60
|
} catch (e) {
|
|
52
|
-
|
|
61
|
+
// 実行に失敗したら空文字に
|
|
62
|
+
res = undefined
|
|
53
63
|
}
|
|
64
|
+
|
|
65
|
+
const out = output.length ? output.join('\n') : (res !== undefined ? String(res) : '')
|
|
66
|
+
newValue = newValue.replace(m, out)
|
|
54
67
|
})
|
|
55
68
|
return newValue
|
|
56
69
|
}
|
|
@@ -59,74 +72,64 @@ export default class Ride {
|
|
|
59
72
|
|
|
60
73
|
// 配列: 各要素を再帰的に処理して <item> でラップ
|
|
61
74
|
if (Array.isArray(value)) {
|
|
62
|
-
return value.map((item: any) => `<item>${this.renderValue(item)}</item>`).join('')
|
|
75
|
+
return value.map((item: any) => `<item>${this.renderValue(item, ctx)}</item>`).join('')
|
|
63
76
|
}
|
|
64
77
|
|
|
65
78
|
// オブジェクト: 属性(@_...) を無視して子要素だけタグ化する
|
|
66
79
|
if (typeof value === 'object') {
|
|
67
80
|
return Object.keys(value)
|
|
68
|
-
.filter(key => !key.startsWith('@_'))
|
|
81
|
+
.filter(key => !key.startsWith('@_'))
|
|
69
82
|
.map(key => {
|
|
70
|
-
// テキストノードは直接中身を出力
|
|
71
83
|
if (key === '#text' || key === '_text') {
|
|
72
|
-
return this.renderValue(value[key])
|
|
84
|
+
return this.renderValue(value[key], ctx)
|
|
73
85
|
}
|
|
74
|
-
return `<${key}>${this.renderValue(value[key])}</${key}>`
|
|
86
|
+
return `<${key}>${this.renderValue(value[key], ctx)}</${key}>`
|
|
75
87
|
}).join('')
|
|
76
88
|
}
|
|
77
89
|
|
|
78
|
-
// その他(数値や真偽値など)はそのまま文字列化
|
|
79
90
|
return String(value)
|
|
80
91
|
}
|
|
81
92
|
|
|
82
93
|
/**
|
|
83
94
|
* タグ名とその値から、属性を正しく反映した HTML を生成する
|
|
84
|
-
* -
|
|
85
|
-
* - value がオブジェクトで属性(@_...)を持つ場合、属性を開タグに組み込む
|
|
95
|
+
* - ctx を渡して { ... } の中で req / server / page / data が参照できる
|
|
86
96
|
*/
|
|
87
|
-
private renderContentTag(tag: string, value: any): string {
|
|
88
|
-
// 配列の場合は各要素で同じタグを生成
|
|
97
|
+
private renderContentTag(tag: string, value: any, ctx: any = {}): string {
|
|
89
98
|
if (Array.isArray(value)) {
|
|
90
|
-
return value.map((item: any) => this.renderContentTag(tag, item)).join('')
|
|
99
|
+
return value.map((item: any) => this.renderContentTag(tag, item, ctx)).join('')
|
|
91
100
|
}
|
|
92
101
|
|
|
93
|
-
// プリミティブ(文字列等)の場合はタグでラップ
|
|
94
102
|
if (value === null || value === undefined || typeof value !== 'object') {
|
|
95
|
-
return `<${tag}>${this.renderValue(value)}</${tag}>`
|
|
103
|
+
return `<${tag}>${this.renderValue(value, ctx)}</${tag}>`
|
|
96
104
|
}
|
|
97
105
|
|
|
98
|
-
// オブジェクトの場合、属性を抽出
|
|
99
106
|
const entries = Object.entries(value)
|
|
100
107
|
const attrEntries = entries.filter(([k]) => k.startsWith('@_'))
|
|
101
108
|
const childEntries = entries.filter(([k]) => !k.startsWith('@_'))
|
|
102
109
|
|
|
103
|
-
// 属性文字列を組み立て(@_ を取り除く)
|
|
104
110
|
const attrs = attrEntries.map(([k, v]) => {
|
|
105
111
|
const name = k.replace(/^@_/, '')
|
|
106
112
|
const safe = String(v).replace(/"/g, '"')
|
|
107
113
|
return ` ${name}="${safe}"`
|
|
108
114
|
}).join('')
|
|
109
115
|
|
|
110
|
-
// 子要素・テキストを組み立て
|
|
111
116
|
let inner = ''
|
|
112
|
-
// 優先してテキストノードを出す
|
|
113
117
|
if (value['#text'] !== undefined) {
|
|
114
|
-
inner += this.renderValue(value['#text'])
|
|
118
|
+
inner += this.renderValue(value['#text'], ctx)
|
|
115
119
|
} else if (value['_text'] !== undefined) {
|
|
116
|
-
inner += this.renderValue(value['_text'])
|
|
120
|
+
inner += this.renderValue(value['_text'], ctx)
|
|
117
121
|
}
|
|
118
122
|
|
|
119
|
-
// その他の子要素をレンダリング
|
|
120
123
|
childEntries.forEach(([k, v]) => {
|
|
121
124
|
if (k === '#text' || k === '_text') return
|
|
122
|
-
inner += `<${k}>${this.renderValue(v)}</${k}>`
|
|
125
|
+
inner += `<${k}>${this.renderValue(v, ctx)}</${k}>`
|
|
123
126
|
})
|
|
124
127
|
|
|
125
128
|
return `<${tag}${attrs}>${inner}</${tag}>`
|
|
126
129
|
}
|
|
127
130
|
|
|
128
131
|
/**
|
|
129
|
-
* XML を解析して
|
|
132
|
+
* XML を解析して pagesTemplates に生の Content を格納(評価はリクエスト時に行う)
|
|
130
133
|
*/
|
|
131
134
|
returnhtml(): string {
|
|
132
135
|
const options = { ignoreAttributes: false }
|
|
@@ -141,24 +144,14 @@ export default class Ride {
|
|
|
141
144
|
const pages = root["Page"]
|
|
142
145
|
if (!pages) return ""
|
|
143
146
|
|
|
144
|
-
// Page が配列か単一かを吸収して配列化
|
|
145
147
|
const pageArray = Array.isArray(pages) ? pages : [pages]
|
|
146
148
|
|
|
147
149
|
pageArray.forEach((page: any) => {
|
|
148
150
|
const route = page['@_route'] || '/'
|
|
149
|
-
|
|
150
|
-
// Content が存在しない場合は空にする
|
|
151
151
|
const content = page['Content'] || {}
|
|
152
152
|
|
|
153
|
-
//
|
|
154
|
-
|
|
155
|
-
const value = content[tag]
|
|
156
|
-
// 属性付きタグなどを正しく扱う helper を使う
|
|
157
|
-
return this.renderContentTag(tag, value)
|
|
158
|
-
}).join('')
|
|
159
|
-
|
|
160
|
-
// ページを格納(タイトルを付与)
|
|
161
|
-
this.pages[route] = `<title>${this.name}</title>` + generatedhtml
|
|
153
|
+
// 生の content を保存しておく(ルートアクセス時に評価する)
|
|
154
|
+
this.pagesTemplates[route] = { content, pageMeta: page }
|
|
162
155
|
})
|
|
163
156
|
} catch (e) {
|
|
164
157
|
console.error("Whoops, an error occurred during analysis : " + e)
|
|
@@ -177,13 +170,38 @@ export default class Ride {
|
|
|
177
170
|
|
|
178
171
|
/**
|
|
179
172
|
* 簡易サーバを起動
|
|
173
|
+
* - リクエスト毎に { ... } の評価を行い、req, query, params, body, server(defaultContext), page が参照可能になる
|
|
180
174
|
*/
|
|
181
175
|
serve(port: number) {
|
|
182
176
|
const app = exp();
|
|
183
177
|
|
|
184
|
-
Object.keys(this.
|
|
178
|
+
Object.keys(this.pagesTemplates).forEach(route => {
|
|
185
179
|
app.get(route, (req, res) => {
|
|
186
|
-
|
|
180
|
+
const tpl = this.pagesTemplates[route]
|
|
181
|
+
const content = tpl?.content || {}
|
|
182
|
+
|
|
183
|
+
// コンテキストを組み立て(デフォルト + リクエスト情報 + ページメタ)
|
|
184
|
+
const ctx = {
|
|
185
|
+
server: this.defaultContext,
|
|
186
|
+
req: {
|
|
187
|
+
params: req.params,
|
|
188
|
+
query: req.query,
|
|
189
|
+
path: req.path,
|
|
190
|
+
headers: req.headers
|
|
191
|
+
},
|
|
192
|
+
page: tpl.pageMeta || {},
|
|
193
|
+
// 任意のデータを追加できるように空の data を用意
|
|
194
|
+
data: {}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// 各タグを評価して結合
|
|
198
|
+
const generatedhtml = Object.keys(content).map(tag => {
|
|
199
|
+
const value = content[tag]
|
|
200
|
+
return this.renderContentTag(tag, value, ctx)
|
|
201
|
+
}).join('')
|
|
202
|
+
|
|
203
|
+
// タイトルを付与して送信
|
|
204
|
+
res.send(`<title>${this.name}</title>` + generatedhtml)
|
|
187
205
|
})
|
|
188
206
|
})
|
|
189
207
|
|
|
@@ -194,7 +212,11 @@ export default class Ride {
|
|
|
194
212
|
return app
|
|
195
213
|
}
|
|
196
214
|
|
|
215
|
+
/**
|
|
216
|
+
* サーバ側で渡したい既定のコンテキストを設定する
|
|
217
|
+
* 例: ride.config({ currentUser: {...}, env: process.env.NODE_ENV })
|
|
218
|
+
*/
|
|
197
219
|
config(options: object) {
|
|
198
|
-
|
|
220
|
+
this.defaultContext = Object.assign({}, this.defaultContext, options)
|
|
199
221
|
}
|
|
200
222
|
}
|