@tenjuu99/blog 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/.env.prod.sample +8 -0
- package/.env.sample +8 -0
- package/.github/workflows/deploy.yml.sample +51 -0
- package/.github/workflows/github-page.yml +47 -0
- package/.gitignore +9 -0
- package/README.md +145 -0
- package/generate.js +11 -0
- package/helper/add.js +3 -0
- package/helper/index.js +53 -0
- package/lib/applyTemplate.js +41 -0
- package/lib/cssGenerator.js +61 -0
- package/lib/dir.js +15 -0
- package/lib/distribute.js +35 -0
- package/lib/filter.js +167 -0
- package/lib/includeFilter.js +38 -0
- package/lib/indexer.js +82 -0
- package/lib/minify.js +22 -0
- package/lib/replaceIfFilter.js +67 -0
- package/lib/replaceScriptFilter.js +71 -0
- package/package.json +40 -0
- package/performance.js +22 -0
- package/server.js +48 -0
- package/src-sample/css/color.css +5 -0
- package/src-sample/css/layout.css +143 -0
- package/src-sample/css/markdown.css +49 -0
- package/src-sample/css/page.css +0 -0
- package/src-sample/css/reset.css +2 -0
- package/src-sample/helper/index.js +3 -0
- package/src-sample/image/.gitkeep +0 -0
- package/src-sample/pages/abc.md +5 -0
- package/src-sample/pages/fuga.md +3 -0
- package/src-sample/pages/hogehoge.md +8 -0
- package/src-sample/pages/html_render.html +20 -0
- package/src-sample/pages/index.md +35 -0
- package/src-sample/pages/notfound.md +8 -0
- package/src-sample/pages/rss.md +8 -0
- package/src-sample/pages/sample.md +145 -0
- package/src-sample/template/css.html +6 -0
- package/src-sample/template/default.html +55 -0
- package/src-sample/template/footer.html +7 -0
- package/src-sample/template/gtag.html +9 -0
- package/src-sample/template/index.html +24 -0
- package/src-sample/template/prevNext.html +35 -0
- package/src-sample/template/rss.xml +41 -0
package/.env.prod.sample
ADDED
package/.env.sample
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
name: Build html and deploy to S3
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches:
|
|
5
|
+
- main
|
|
6
|
+
jobs:
|
|
7
|
+
build:
|
|
8
|
+
runs-on: ubuntu-latest
|
|
9
|
+
steps:
|
|
10
|
+
- name: Checkout
|
|
11
|
+
uses: actions/checkout@master
|
|
12
|
+
|
|
13
|
+
- name: npm install
|
|
14
|
+
run: npm install --production
|
|
15
|
+
|
|
16
|
+
- name: Build
|
|
17
|
+
run: npm run generate:prod
|
|
18
|
+
|
|
19
|
+
- name: Deploy
|
|
20
|
+
env:
|
|
21
|
+
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
|
22
|
+
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
|
23
|
+
run: |
|
|
24
|
+
aws s3 sync \
|
|
25
|
+
--region ap-northeast-1 \
|
|
26
|
+
--exact-timestamps \
|
|
27
|
+
--exclude .gitignore \
|
|
28
|
+
--exclude '*.css' \
|
|
29
|
+
--exclude 'image/' \
|
|
30
|
+
--delete \
|
|
31
|
+
--metadata-directive REPLACE \
|
|
32
|
+
--cache-control "max-age=86400, no-cache, public" \
|
|
33
|
+
dist/ s3://${{ secrets.S3_URL }}/
|
|
34
|
+
|
|
35
|
+
aws s3 sync \
|
|
36
|
+
--region ap-northeast-1 \
|
|
37
|
+
--exact-timestamps \
|
|
38
|
+
--exclude .gitignore \
|
|
39
|
+
--delete \
|
|
40
|
+
--metadata-directive REPLACE \
|
|
41
|
+
--cache-control "max-age=31536000, public" \
|
|
42
|
+
dist/css/ s3://${{ secrets.S3_URL }}/css/
|
|
43
|
+
|
|
44
|
+
aws s3 sync \
|
|
45
|
+
--region ap-northeast-1 \
|
|
46
|
+
--exact-timestamps \
|
|
47
|
+
--exclude .gitignore \
|
|
48
|
+
--delete \
|
|
49
|
+
--metadata-directive REPLACE \
|
|
50
|
+
--cache-control "max-age=86400, public" \
|
|
51
|
+
dist/image/ s3://${{ secrets.S3_URL }}/image/
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Simple workflow for deploying static content to GitHub Pages
|
|
2
|
+
name: Deploy static content to Pages
|
|
3
|
+
|
|
4
|
+
on:
|
|
5
|
+
push:
|
|
6
|
+
branches:
|
|
7
|
+
- main
|
|
8
|
+
workflow_dispatch:
|
|
9
|
+
|
|
10
|
+
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
pages: write
|
|
14
|
+
id-token: write
|
|
15
|
+
|
|
16
|
+
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
|
|
17
|
+
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
|
|
18
|
+
concurrency:
|
|
19
|
+
group: "pages"
|
|
20
|
+
cancel-in-progress: false
|
|
21
|
+
|
|
22
|
+
jobs:
|
|
23
|
+
deploy:
|
|
24
|
+
environment:
|
|
25
|
+
name: github-pages
|
|
26
|
+
url: ${{ steps.deployment.outputs.page_url }}
|
|
27
|
+
runs-on: ubuntu-latest
|
|
28
|
+
steps:
|
|
29
|
+
- name: Checkout
|
|
30
|
+
uses: actions/checkout@master
|
|
31
|
+
|
|
32
|
+
- name: npm install
|
|
33
|
+
run: npm install --production
|
|
34
|
+
|
|
35
|
+
- name: Build
|
|
36
|
+
run: cp .env.prod.sample .env && npm run generate
|
|
37
|
+
|
|
38
|
+
- name: Setup Pages
|
|
39
|
+
uses: actions/configure-pages@v4
|
|
40
|
+
- name: Upload artifact
|
|
41
|
+
uses: actions/upload-pages-artifact@v3
|
|
42
|
+
with:
|
|
43
|
+
# Upload dist
|
|
44
|
+
path: './dist/'
|
|
45
|
+
- name: Deploy to GitHub Pages
|
|
46
|
+
id: deployment
|
|
47
|
+
uses: actions/deploy-pages@v4
|
package/.gitignore
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# blog template
|
|
2
|
+
|
|
3
|
+
## プレビュー
|
|
4
|
+
|
|
5
|
+
以下のコマンドでプレビューできます。
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
npm run watch
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
`http://localhost:8000/`
|
|
12
|
+
|
|
13
|
+
デフォルトでは8000ポートを利用しますが、環境変数を使ってポート番号を上書きできます。
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
PORT=8001 npm run watch
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## 記事を書く
|
|
20
|
+
|
|
21
|
+
`./data/` 以下にマークダウンファイルを入稿します。
|
|
22
|
+
|
|
23
|
+
ファイル冒頭を `---` でくくると変数を指定できます。
|
|
24
|
+
`title` と `url` と `published`が必須です。
|
|
25
|
+
`url` は出力先になります。
|
|
26
|
+
`published` は index ファイルがソートするのに利用します。
|
|
27
|
+
|
|
28
|
+
次のような内容で、 `data/sample_article.md` などに保存すると `http://localhost:8000/sample_article` でアクセスできるようになります。
|
|
29
|
+
|
|
30
|
+
```markdown
|
|
31
|
+
---
|
|
32
|
+
title: sample article
|
|
33
|
+
url: /sample_article
|
|
34
|
+
published: 2024/03/18 00:00
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
トップレベル見出しは `title` 変数が利用されます。
|
|
38
|
+
変えたい場合は template/default.html を修正してください。
|
|
39
|
+
|
|
40
|
+
## サンプル記事
|
|
41
|
+
|
|
42
|
+
これはサンプル記事です。ブログ開始のときに削除してください。
|
|
43
|
+
|
|
44
|
+
## マークダウン
|
|
45
|
+
|
|
46
|
+
記事は、マークダウン記法で記述し、 `.md` ファイルで保存してください。
|
|
47
|
+
|
|
48
|
+
リンクは次のように記述します。
|
|
49
|
+
[アンカーテキストはこちら](http://example.com)
|
|
50
|
+
|
|
51
|
+
リストは次のように記述します。
|
|
52
|
+
* リスト1
|
|
53
|
+
* リスト2
|
|
54
|
+
* リスト3
|
|
55
|
+
|
|
56
|
+
画像は、 `data/image/` 以下に配置して、パスを指定してください。
|
|
57
|
+
|
|
58
|
+
`data/image/sample.jpg` などがあれば、次の指定になります。
|
|
59
|
+
``
|
|
60
|
+
|
|
61
|
+

|
|
62
|
+
|
|
63
|
+
### 見出し3
|
|
64
|
+
|
|
65
|
+
スクリプトを記述することができます。
|
|
66
|
+
|
|
67
|
+
{script}
|
|
68
|
+
return (new Date()).toString()
|
|
69
|
+
{/script}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
[data/sample.md](data/sample.md) にサンプルを置いています。
|
|
73
|
+
|
|
74
|
+
## テンプレートエンジン
|
|
75
|
+
|
|
76
|
+
### 変数
|
|
77
|
+
|
|
78
|
+
コメントに変数を記述できます。
|
|
79
|
+
|
|
80
|
+
```markdown
|
|
81
|
+
---
|
|
82
|
+
foo: fooVariableContent
|
|
83
|
+
bar: varVariableContent
|
|
84
|
+
---
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
and in template
|
|
88
|
+
|
|
89
|
+
```html
|
|
90
|
+
<!DOCTYPE html>
|
|
91
|
+
<html lang="en">
|
|
92
|
+
<head>
|
|
93
|
+
<meta charset="UTF-8">
|
|
94
|
+
<title></title>
|
|
95
|
+
</head>
|
|
96
|
+
<body>
|
|
97
|
+
{{foo}}
|
|
98
|
+
{{bar}}
|
|
99
|
+
</body>
|
|
100
|
+
</html>
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`{{foo}}` と `{{bar}}` は `'fooVariableContent'` と `'barVariableContent'` に置換されます。
|
|
104
|
+
|
|
105
|
+
### IF
|
|
106
|
+
|
|
107
|
+
```markdown
|
|
108
|
+
---
|
|
109
|
+
someVariable: true
|
|
110
|
+
---
|
|
111
|
+
{if someVariable}
|
|
112
|
+
This content will be present.
|
|
113
|
+
{/if}
|
|
114
|
+
|
|
115
|
+
{if undefinedValue}
|
|
116
|
+
This content will be removed.
|
|
117
|
+
{else}
|
|
118
|
+
This is else content.
|
|
119
|
+
{/if}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### SCRIPT
|
|
123
|
+
|
|
124
|
+
```markdown
|
|
125
|
+
{script}
|
|
126
|
+
// write javascript and return value
|
|
127
|
+
{/script}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## デプロイ
|
|
131
|
+
|
|
132
|
+
AWS S3 にデプロイするには、 `.github/workflow/deploy.yml.sample` を `.github/workflow/deploy.yml` にコピーします。
|
|
133
|
+
また、 `.env.prod` を作成して、以下の値を登録します。
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
SITE_NAME: テストサイト
|
|
137
|
+
URL_BASE: https://example.com
|
|
138
|
+
GTAG_ID: G-xxxx
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
GitHub のリポジトリで、`settings > Secrets and variables > Actions` から、以下の変数を登録します。
|
|
142
|
+
|
|
143
|
+
* `AWS_ACCESS_KEY_ID`
|
|
144
|
+
* `AWS_SECRET_ACCESS_KEY_ID`
|
|
145
|
+
* `S3_URL`
|
package/generate.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict"
|
|
2
|
+
import distribute from './lib/distribute.js'
|
|
3
|
+
import { indexing } from './lib/indexer.js'
|
|
4
|
+
import { srcDir, distDir } from './lib/dir.js'
|
|
5
|
+
|
|
6
|
+
const start = performance.now()
|
|
7
|
+
const data = await indexing(srcDir + '/pages/')
|
|
8
|
+
|
|
9
|
+
await distribute(data, srcDir, distDir)
|
|
10
|
+
const end = performance.now()
|
|
11
|
+
console.log('build: ' + (end - start) + "ms")
|
package/helper/add.js
ADDED
package/helper/index.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { allData } from "../lib/indexer.js";
|
|
2
|
+
import { replaceVariablesFilter } from "../lib/filter.js";
|
|
3
|
+
|
|
4
|
+
export function readIndex (filter = null) {
|
|
5
|
+
const data = Object.entries(allData)
|
|
6
|
+
.sort((a, b) => new Date(b[1].published) - new Date(a[1].published))
|
|
7
|
+
return filter
|
|
8
|
+
? data.filter(v => v[0].indexOf(filter) === 0).map(v => v[1])
|
|
9
|
+
: data.map(v => v[1])
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function dateFormat(dateString) {
|
|
13
|
+
const date = new Date(dateString)
|
|
14
|
+
return `${date.getFullYear()}年${date.getMonth() + 1}月${date.getDate()}日`
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function render(text, variables) {
|
|
18
|
+
return replaceVariablesFilter(text, variables)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getPageData(name) {
|
|
22
|
+
return allData[name]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let indexedItemsSorted = null
|
|
26
|
+
export function indexedItems() {
|
|
27
|
+
if (indexedItemsSorted) {
|
|
28
|
+
return indexedItemsSorted
|
|
29
|
+
}
|
|
30
|
+
const sorted = readIndex()
|
|
31
|
+
.filter(v => v.index && v.published != '1970-01-01')
|
|
32
|
+
.sort((a, b) => new Date(a.published) - new Date(b.published))
|
|
33
|
+
let prev, next
|
|
34
|
+
for (const item of sorted) {
|
|
35
|
+
if (prev) {
|
|
36
|
+
prev.next = {
|
|
37
|
+
name: item.name,
|
|
38
|
+
published: item.published,
|
|
39
|
+
url: item.url,
|
|
40
|
+
title: item.title,
|
|
41
|
+
}
|
|
42
|
+
item.prev = {
|
|
43
|
+
name: prev.name,
|
|
44
|
+
published: prev.published,
|
|
45
|
+
url: prev.url,
|
|
46
|
+
title: prev.title,
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
prev = item
|
|
50
|
+
}
|
|
51
|
+
indexedItemsSorted = sorted
|
|
52
|
+
return indexedItemsSorted
|
|
53
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict"
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import applyCss from './cssGenerator.js'
|
|
4
|
+
import {
|
|
5
|
+
replaceIfFilter,
|
|
6
|
+
replaceScriptFilter,
|
|
7
|
+
replaceVariablesFilter,
|
|
8
|
+
includeFilter
|
|
9
|
+
} from './filter.js'
|
|
10
|
+
import { marked } from "marked";
|
|
11
|
+
import { templateDir } from './dir.js'
|
|
12
|
+
|
|
13
|
+
const templates = {}
|
|
14
|
+
|
|
15
|
+
const applyTemplate = async (name = 'default.html') => {
|
|
16
|
+
if (templates[name]) {
|
|
17
|
+
return templates[name]
|
|
18
|
+
}
|
|
19
|
+
let templateContent = await fs.readFile(`${templateDir}/${name}`, 'utf8')
|
|
20
|
+
templateContent = await includeFilter(templateContent)
|
|
21
|
+
templateContent = await applyCss(templateContent)
|
|
22
|
+
templates[name] = templateContent
|
|
23
|
+
return templateContent
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const render = async (templateName, data) => {
|
|
27
|
+
let template = await applyTemplate(templateName)
|
|
28
|
+
template = replaceIfFilter(template, data)
|
|
29
|
+
template = await replaceScriptFilter(template, data)
|
|
30
|
+
|
|
31
|
+
let markdown = data.markdown
|
|
32
|
+
markdown = await includeFilter(markdown)
|
|
33
|
+
markdown = await replaceIfFilter(markdown, data)
|
|
34
|
+
markdown = await replaceScriptFilter(markdown, data)
|
|
35
|
+
data.markdown = data.__filetype === 'md' ? marked.parse(markdown) : markdown
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
return replaceVariablesFilter(template, data)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default render
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict"
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import { minifyCss } from './minify.js'
|
|
4
|
+
import { createHash } from 'crypto'
|
|
5
|
+
import path from 'path'
|
|
6
|
+
import { distDir as distRoot, cssDir } from './dir.js'
|
|
7
|
+
|
|
8
|
+
const cacheBuster = {}
|
|
9
|
+
const cacheBusterQuery = 't'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {string} src
|
|
13
|
+
* @param {string} dist
|
|
14
|
+
* @returns {Promise<string>}
|
|
15
|
+
*/
|
|
16
|
+
const cssGenerator = async (src, dist) => {
|
|
17
|
+
const key = `${dist}${src}`
|
|
18
|
+
// 生成済みの場合はそのまま返す
|
|
19
|
+
for (const a in cacheBuster) {
|
|
20
|
+
if (a === key) {
|
|
21
|
+
return cacheBuster[key]
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
let css = ''
|
|
25
|
+
for (const cssFile of src.split(',')) {
|
|
26
|
+
css += await fs.readFile(`${cssDir}/${cssFile}`)
|
|
27
|
+
}
|
|
28
|
+
css = minifyCss(css)
|
|
29
|
+
cacheBuster[key] = createHash('md5').update(css).digest('hex')
|
|
30
|
+
|
|
31
|
+
return await fs.mkdir(`${distRoot}${path.dirname(dist)}`, { recursive: true }).then(async () => {
|
|
32
|
+
await fs.writeFile(`${distRoot}${dist}`, css)
|
|
33
|
+
console.log(`generate ${src} => ${distRoot}${dist}`)
|
|
34
|
+
return cacheBuster[key]
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 次のような記述を想定している。
|
|
40
|
+
* <link rel="stylesheet" href="${/css/layout.css:/css/base.css,/css/page.css}">
|
|
41
|
+
* href の記述は ${dist:src} の関係になっている。
|
|
42
|
+
*
|
|
43
|
+
* これを
|
|
44
|
+
* <link rel="stylesheet" href="/css/layout.css?t=daddfiao3901239dasei12b">
|
|
45
|
+
* に変換する。
|
|
46
|
+
*
|
|
47
|
+
* @param {string} text
|
|
48
|
+
* @return {string}
|
|
49
|
+
*/
|
|
50
|
+
const applyCss = async (text) => {
|
|
51
|
+
const target = [...text.matchAll(/\${([\w-_/]+\.css)<<([\w-_/,.]+\.css)}/g)].map(val => {
|
|
52
|
+
return { matched: val[0], dist: val[1], src: val[2] }
|
|
53
|
+
})
|
|
54
|
+
for (const cssDist of target) {
|
|
55
|
+
const cacheBuster = await cssGenerator(cssDist.src, cssDist.dist)
|
|
56
|
+
text = text.replace(cssDist.matched, `${process.env.RELATIVE_PATH || ''}${cssDist.dist}?${cacheBusterQuery}=${cacheBuster}`)
|
|
57
|
+
}
|
|
58
|
+
return text
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export default applyCss
|
package/lib/dir.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const rootDir = process.cwd()
|
|
2
|
+
const srcDir = `${rootDir}/${process.env.SRC_DIR}`
|
|
3
|
+
const distDir = `${rootDir}/${process.env.DIST_DIR}`
|
|
4
|
+
const templateDir = `${rootDir}/${process.env.SRC_DIR}/template`
|
|
5
|
+
const cssDir = `${rootDir}/${process.env.SRC_DIR}/css`
|
|
6
|
+
const cacheDir = `${rootDir}/.cache`
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
rootDir,
|
|
10
|
+
srcDir,
|
|
11
|
+
distDir,
|
|
12
|
+
templateDir,
|
|
13
|
+
cssDir,
|
|
14
|
+
cacheDir,
|
|
15
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict"
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import { minifyHtml } from './minify.js'
|
|
5
|
+
import render from './applyTemplate.js'
|
|
6
|
+
|
|
7
|
+
const distribute = async (data, srcDir, distDir) => {
|
|
8
|
+
if (data['__deleted']) {
|
|
9
|
+
for (const i in data['__deleted']) {
|
|
10
|
+
console.log(`unlink ${distDir}${data['__deleted'][i].__output}`)
|
|
11
|
+
fs.unlink(`${distDir}${data['__deleted'][i].__output}`)
|
|
12
|
+
}
|
|
13
|
+
delete data['__deleted']
|
|
14
|
+
}
|
|
15
|
+
for (const name in data) {
|
|
16
|
+
const template = data[name].template
|
|
17
|
+
const rendered = await render(template, data[name])
|
|
18
|
+
let writeTo = `${distDir}${data[name].__output}`
|
|
19
|
+
fs.mkdir(path.dirname(writeTo), { recursive: true}).then(() => {
|
|
20
|
+
fs.writeFile(writeTo, minifyHtml(rendered))
|
|
21
|
+
console.log(`generate ${writeTo}`)
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
const distributeRaw = process.env.DISTRIBUTE_RAW.split(',')
|
|
25
|
+
distributeRaw.forEach((copyDir) => {
|
|
26
|
+
fs.readdir(`${srcDir}/${copyDir}/`).then(async files => {
|
|
27
|
+
await fs.stat(`${distDir}/${copyDir}/`).catch(async err => await fs.mkdir(`${distDir}/${copyDir}/`))
|
|
28
|
+
files.forEach(file => {
|
|
29
|
+
fs.copyFile(`${srcDir}/${copyDir}/${file}`, `${distDir}/${copyDir}/${file}`)
|
|
30
|
+
})
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export default distribute
|
package/lib/filter.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import * as helper from '../helper/index.js'
|
|
2
|
+
import includeFilter from './includeFilter.js'
|
|
3
|
+
import { srcDir } from './dir.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @param {string} text
|
|
7
|
+
* @params {object} variables
|
|
8
|
+
* @return {text}
|
|
9
|
+
*/
|
|
10
|
+
const replaceVariablesFilter = (text, variables) => {
|
|
11
|
+
const matched = [...text.matchAll(/{{\s?([\w\d-_\(\)]+)\s?}}/g)]
|
|
12
|
+
const replace = Object.fromEntries(matched.map(match => [match[1].toLowerCase(), match[0]]))
|
|
13
|
+
let replaced = text
|
|
14
|
+
for (const elm in replace) {
|
|
15
|
+
const toBeReplace = replace[elm]
|
|
16
|
+
const toBeReplaceScript = toBeReplace.match(/([\w\d_-]+)\((.+)\)/)
|
|
17
|
+
if (toBeReplaceScript) {
|
|
18
|
+
const func = toBeReplaceScript[1]
|
|
19
|
+
const variable = toBeReplaceScript[2]
|
|
20
|
+
if (!helper[func]) {
|
|
21
|
+
throw new Exception('helper function is missing. function name: ' + func);
|
|
22
|
+
}
|
|
23
|
+
const replaceText = helper[func](variables[variable.toLowerCase()])
|
|
24
|
+
replaced = replaced.replaceAll(toBeReplace, replaceText)
|
|
25
|
+
} else {
|
|
26
|
+
replaced = replaced.replaceAll(toBeReplace, variables[elm])
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return replaced
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {string} condition
|
|
34
|
+
* @params {object} variables
|
|
35
|
+
* @return {bool}
|
|
36
|
+
*/
|
|
37
|
+
const ifConditionEvaluator = (condition, variables) => {
|
|
38
|
+
if (condition.includes('=')) {
|
|
39
|
+
const segmented = condition.match(/(?<left>[\S]+)\s(?<operator>!=|==)\s(?<right>[\S]+)/)
|
|
40
|
+
let {left, operator, right} = segmented.groups
|
|
41
|
+
if (variables.hasOwnProperty(left)) {
|
|
42
|
+
left = variables[left]
|
|
43
|
+
} else {
|
|
44
|
+
try {
|
|
45
|
+
left = eval(left)
|
|
46
|
+
} catch (e) {
|
|
47
|
+
left = undefined
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (variables.hasOwnProperty(right)) {
|
|
51
|
+
right = variables[right]
|
|
52
|
+
} else {
|
|
53
|
+
try {
|
|
54
|
+
right = eval(right)
|
|
55
|
+
} catch (e) {
|
|
56
|
+
right = undefined
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
switch (operator) {
|
|
60
|
+
case '==':
|
|
61
|
+
return left == right
|
|
62
|
+
case '!=':
|
|
63
|
+
return left != right
|
|
64
|
+
}
|
|
65
|
+
} else {
|
|
66
|
+
if (variables.hasOwnProperty(condition)) {
|
|
67
|
+
return !!variables[condition]
|
|
68
|
+
}
|
|
69
|
+
return false
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* @param {string} text
|
|
75
|
+
* @param {object} variables
|
|
76
|
+
* @returns {string}
|
|
77
|
+
*/
|
|
78
|
+
const replaceIfFilter = (text, variables) => {
|
|
79
|
+
const ifRegexp = new RegExp(/(\{|\<)if\s(?<condition>[\s\S]+?)(}|>)(?<content>[\s\S]*?)((\{|\<)else(\}|\>)(?<elsecontent>[\s\S]*?))?(\{|\<)\/if(\>|})/g)
|
|
80
|
+
const matched = [...text.matchAll(ifRegexp)]
|
|
81
|
+
for (const item of matched) {
|
|
82
|
+
const target = item[0]
|
|
83
|
+
const content = item.groups.content
|
|
84
|
+
const elseContent = item.groups.elsecontent
|
|
85
|
+
if (ifConditionEvaluator(item.groups.condition, variables)) {
|
|
86
|
+
text = text.replace(target, content)
|
|
87
|
+
} else if (elseContent) {
|
|
88
|
+
text = text.replace(target, elseContent)
|
|
89
|
+
} else {
|
|
90
|
+
text = text.replace(target, '')
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return text
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 配列を再帰的に順不同リストに変換する
|
|
98
|
+
* @param {Array|string} arrayOrText
|
|
99
|
+
* @returns {mixed}
|
|
100
|
+
*/
|
|
101
|
+
const arrayToList = (arrayOrText) => {
|
|
102
|
+
if (typeof arrayOrText === 'string') {
|
|
103
|
+
return `<li>${arrayOrText}</li>`
|
|
104
|
+
}
|
|
105
|
+
if (Array.isArray(arrayOrText)) {
|
|
106
|
+
let resultListText = '<ul>'
|
|
107
|
+
for (const item of arrayOrText) {
|
|
108
|
+
if (Array.isArray(item)) {
|
|
109
|
+
resultListText += `<li>${arrayToList(item)}</li>`
|
|
110
|
+
} else {
|
|
111
|
+
resultListText += `<li>${item}</li>`
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
resultListText += '</ul>'
|
|
115
|
+
arrayOrText = resultListText
|
|
116
|
+
}
|
|
117
|
+
return arrayOrText
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const replaceScriptFilter = async (text, variables) => {
|
|
121
|
+
let replaced = text
|
|
122
|
+
const scriptRegexp = new RegExp(/({script}|\<script\s.*type="ssg".*>)(?<script>[\s\S]*?)(\{\/script}|\<\/script>)/g)
|
|
123
|
+
const scripts = [...text.matchAll(scriptRegexp)].map((matched) => {
|
|
124
|
+
return {
|
|
125
|
+
replace: matched[0],
|
|
126
|
+
script: matched.groups.script.trim("\n"),
|
|
127
|
+
}
|
|
128
|
+
})
|
|
129
|
+
for (const script of scripts) {
|
|
130
|
+
let helperMerged = {...helper}
|
|
131
|
+
// env.HELPER が定義されていれば追加ヘルパーとして扱う
|
|
132
|
+
if (process.env.HELPER) {
|
|
133
|
+
const additional = await import(`${srcDir}/${process.env.HELPER}`)
|
|
134
|
+
helperMerged = Object.assign(helperMerged, additional)
|
|
135
|
+
}
|
|
136
|
+
let result = new Function('helper', 'variables', script.script)(helperMerged, variables)
|
|
137
|
+
if (result instanceof Promise) {
|
|
138
|
+
result = await result
|
|
139
|
+
}
|
|
140
|
+
if (typeof result === 'undefined') {
|
|
141
|
+
result = null
|
|
142
|
+
}
|
|
143
|
+
if (Array.isArray(result)) {
|
|
144
|
+
result = arrayToList(result)
|
|
145
|
+
} else if (typeof result === 'object') {
|
|
146
|
+
const resultText = []
|
|
147
|
+
for (const key in result) {
|
|
148
|
+
resultText.push(`<h3>${key}</h3>`)
|
|
149
|
+
if (Array.isArray(result[key])) {
|
|
150
|
+
resultText.push(arrayToList(result[key]))
|
|
151
|
+
} else {
|
|
152
|
+
resultText.push(`<p>${result[key]}</p>`)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
result = resultText.join('\n')
|
|
156
|
+
}
|
|
157
|
+
replaced = replaced.replace(script.replace, result)
|
|
158
|
+
}
|
|
159
|
+
return replaced
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export {
|
|
163
|
+
replaceIfFilter,
|
|
164
|
+
replaceScriptFilter,
|
|
165
|
+
replaceVariablesFilter,
|
|
166
|
+
includeFilter
|
|
167
|
+
}
|