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.
Files changed (2) hide show
  1. package/index.ts +130 -96
  2. 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 // 読み込んだ XML ファイルの内容(生の文字列)
15
- parsedJson: any // fast-xml-parser によるパース結果(可変な構造のため any)
16
- pages: Record<string, string> // ルートパス => 生成した HTML のマップ
17
- name: string // サイト/アプリケーション名(<title> に利用)
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
- * XML を解析して pages に HTML を格納する。
30
- * - fast-xml-parser を使って XML を JSON に変換
31
- * - Page が配列か単一かで処理を分岐
32
- * - Content の各タグに対して文字列・配列・オブジェクトに応じて HTML を生成
33
- * - `{...}` の中身は一時ファイル runs.js に書き出し node で実行、その出力で置換する(元の実装に合わせた動作)
27
+ * 値を再帰的に HTML に変換するユーティリティ
28
+ * - 文字列: `{...}` を評価して置換(既存挙動を維持)
29
+ * - 配列: 各要素を <item> でラップして連結
30
+ * - オブジェクト: 各キーをタグ名として再帰的にレンダリング
34
31
  *
35
- * 注意: 現状は評価のために runs.js を書き込んで node で実行します。外部入力を評価すると危険です。
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, '&quot;')
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
- // Root.Page が配列かどうかで分岐
45
- if (Array.isArray(this.parsedJson["Root"]["Page"])) {
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
- return `<${tags}>${value}</${tags}>`
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
- * 簡易サーバを起動し、生成済み pages をルーティングする
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-riner",
3
- "version": "1.2.3",
3
+ "version": "1.3.1",
4
4
  "main": "index.ts",
5
5
  "scripts": {
6
6
  "test": "echo \"Error: no test specified\" && exit 1",