@edemaine/shtml2html
Advanced tools
Comparing version 0.0.0 to 0.1.0
{ | ||
"name": "@edemaine/shtml2html", | ||
"version": "0.0.0", | ||
"version": "0.1.0", | ||
"description": "Convert Apache server-side rendered HTML to static HTML", | ||
"type": "module", | ||
"main": "shtml2html.js", | ||
@@ -9,3 +10,6 @@ "bin": { | ||
}, | ||
"files": ["shtml2html.civet", "tsconfig.json"], | ||
"files": [ | ||
"shtml2html.civet", | ||
"tsconfig.json" | ||
], | ||
"scripts": { | ||
@@ -22,4 +26,5 @@ "prepare": "civet -c shtml2html.civet --js -o .js", | ||
"@danielx/civet": "^0.6.6", | ||
"@types/node": "^20.2.5" | ||
"@types/node": "^20.2.5", | ||
"@types/strftime": "^0.9.4" | ||
} | ||
} |
@@ -21,3 +21,3 @@ # shtml2html | ||
## Supported Features | ||
## Supported Directives | ||
@@ -28,6 +28,14 @@ * `<!--#include file="path"-->` includes a non-relative path | ||
TODO: way to specify document root. | ||
* `<!--#include virtual="path1" virtual="path2"-->` for multiple inclusions | ||
* `<!--#flastmod file="path"-->`: | ||
modified date of non-relative path (via `timefmt`) | ||
* `<!--#flastmod virtual="path"-->`: | ||
modified date of relative path (via `timefmt`) | ||
* `<!--#fsize file="path"-->`: size of non-relative path (via `sizefmt`) | ||
* `<!--#fsize virtual="path"-->`: size of relative path (via `sizefmt`) | ||
* `<!--#config key="value"-->` where `key` is among: | ||
* `echomsg`: message for unsupported `#echo` | ||
* `errormsg` (not currently used) | ||
* `sizefmt` (not currently used) | ||
* `errormsg`: error to include in the file when a directive fails | ||
(a more descriptive error message should also be printed to console) | ||
* `sizefmt`: format for `#fsize`, either "bytes" (default) or "abbrev" | ||
* `timefmt`: strftime format for `LAST_MODIFIED` | ||
@@ -37,1 +45,2 @@ * `<!--#echo var="value"-->` where `value` is among: | ||
* `LAST_MODIFIED`: modified date of `.shtml` input (via `timefmt`) | ||
* `<!--#comment ...-->` gets removed |
#!/usr/bin/env node | ||
import fs from "fs" | ||
import path from "path" | ||
import strftime from "strftime" | ||
import url from "url" | ||
const fs = require('fs') | ||
const path = require('path') | ||
const strftime = require('strftime') | ||
class Convert { | ||
@@ -17,3 +17,2 @@ // Maintains state of converter, including configuration and variables. | ||
this.rootFilename = rootFilename1; | ||
const stat = fs.statSync(this.rootFilename) | ||
this.config = { | ||
@@ -27,39 +26,132 @@ echomsg: '(none)', | ||
DOCUMENT_NAME: outFilename, | ||
LAST_MODIFIED: () => { | ||
return strftime(this.config.timefmt, stat.mtime) | ||
}, | ||
LAST_MODIFIED: () => this.flastmod(this.rootFilename), | ||
} | ||
} | ||
convertToString(filename = this.rootFilename) { | ||
let html = fs.readFileSync(filename, {encoding: 'utf8'}) | ||
while(true) { | ||
const old = html | ||
html = html | ||
.replace(/<!--#include\s+(file|virtual)\s*=\s*"([^"]+)"\s*-->/g, | ||
(match, type, url) => { | ||
if (type == 'file' && (url.includes('../') || path.isAbsolute(url))) { | ||
console.log(`* Invalid #include file: ${url}`) | ||
return this.config.errormsg | ||
flastmod(filename) { | ||
try { | ||
return strftime(this.config.timefmt, fs.statSync(filename).mtime) | ||
} | ||
catch (error) { | ||
console.log(`!! flastmod error: ${error}`) | ||
return this.config.errormsg | ||
} | ||
} | ||
fsize(filename) { | ||
try { | ||
const size = fs.statSync(filename).size | ||
switch(this.config.sizefmt) { | ||
case 'bytes': { | ||
return `${size}` | ||
} | ||
case 'abbrev': { | ||
if (size < 973) { | ||
return `${size}` | ||
} | ||
else if (size < 973 * 1024) { | ||
return `${(size / 1024).toFixed(1)}K` | ||
} | ||
else { | ||
return this.convertToString(path.join(path.dirname(filename), url)) | ||
return `${(size / (1024 * 1024)).toFixed(1)}M` | ||
} | ||
},) | ||
.replace(/<!--#config\s+(\w+)="([^"]+)"\s*-->/g, | ||
} | ||
default: { | ||
console.log(`!! Invalid sizefmt: ${this.config.sizefmt}`) | ||
return this.config.errormsg | ||
} | ||
} | ||
} | ||
catch (error) { | ||
console.log(`!! fsize error: ${error}`) | ||
return this.config.errormsg | ||
} | ||
} | ||
convertToString(filename = this.rootFilename) { | ||
return fs.readFileSync(filename, {encoding: 'utf8'}) | ||
.replace(/<!--\#(include|flastmod|fsize|config|echo)\b(.*?)-->/g, (match, command, rest) => { | ||
// #comment can have arbitrary content | ||
if (command == 'comment') { return '' }; | ||
// Parse arguments | ||
const args = [] | ||
rest = rest.replace(/\s*([\w\-]+)\s*=\s*"([^"]*)"\s*/g, | ||
(match, key, value) => { | ||
this.config[key] = value | ||
args.push([key, value]) | ||
return '' | ||
},) | ||
.replace(/<!--#echo\s+var="([^"]+)"\s*-->/g, | ||
(match, key) => { | ||
let val = this.vars[key] ?? this.config.echomsg | ||
if (val instanceof Function) { val = val() } | ||
return val | ||
},) | ||
if (old == html) { break } | ||
} | ||
return html | ||
if (rest) { | ||
console.log(`!! Invalid #${command} directive: ${rest}`) | ||
return this.config.errormsg | ||
} | ||
function getArg(desired) { | ||
for (const [key, value] of args) { | ||
if (key == desired) { return value } | ||
} | ||
return undefined | ||
} | ||
const fileCommand = (func, multi) => { | ||
let done = false; | ||
return (()=>{const results=[];for (const [key, value] of args) { | ||
if (done && !multi) { break } | ||
switch(key) { | ||
case 'file': { | ||
if (value.includes('../') || path.isAbsolute(value)) { | ||
console.log(`!! Invalid file argument: ${value}`) | ||
results.push(this.config.errormsg) | ||
} | ||
else { | ||
done = true | ||
results.push(func(path.join(path.dirname(filename), value))) | ||
};break; | ||
} | ||
case 'virtual': { | ||
done = true | ||
results.push(func(path.join(path.dirname(filename), value)));break; | ||
} | ||
default: { | ||
continue | ||
} | ||
} | ||
}; return results})().join('') | ||
} | ||
switch(command) { | ||
// CIVET: shouldn't need parens below | ||
case 'include': { | ||
return fileCommand(this.convertToString.bind(this), true) | ||
} | ||
case 'flastmod': { | ||
return fileCommand(this.flastmod.bind(this)) | ||
} | ||
case 'fsize': { | ||
return fileCommand(this.fsize.bind(this)) | ||
} | ||
case 'config': { | ||
for (const [key, value] of args) { | ||
this.config[key] = value | ||
} | ||
return '' | ||
} | ||
case 'echo': { | ||
const varName = getArg('var') | ||
if (varName != null) { | ||
let val = this.vars[varName] ?? this.config.echomsg | ||
if (val instanceof Function) { val = val() } | ||
return val | ||
} | ||
else { | ||
console.log(`!! #echo directive missing var: ${rest}`) | ||
return this.config.errormsg | ||
} | ||
} | ||
default: { | ||
return this.config.errormsg | ||
} | ||
} | ||
}) | ||
} | ||
} | ||
} // should never happen; to satisfy TypeScript | ||
@@ -76,7 +168,9 @@ function convertFile(filename, outFilename) { | ||
function main() { | ||
const results=[];for (const arg of process.argv.slice(2)) { | ||
results.push(convertFile(arg)) | ||
};return results; | ||
const results1=[];for (const arg of process.argv.slice(2)) { | ||
results1.push(convertFile(arg)) | ||
};return results1; | ||
} | ||
if (require.main == module) { main() } | ||
if (import.meta.url.startsWith('file:') && | ||
fs.realpathSync(process.argv[1]) == | ||
fs.realpathSync(url.fileURLToPath(import.meta.url))) { main() } |
{ | ||
"compilerOptions": { | ||
"module": "nodenext", | ||
"strict": true, | ||
"noImplicitAny": true, | ||
"jsx": "preserve", | ||
"lib": ["es2021", "node"], | ||
"lib": ["es2021"], | ||
"forceConsistentCasingInFileNames": true, | ||
@@ -7,0 +9,0 @@ "resolveJsonModule": true, |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
13054
174
44
Yes
3