@shangxueink/koishi-plugin-puppeteer-without-canvas
Version:
147 lines (126 loc) • 3.65 kB
text/typescript
import { Context, h } from 'koishi'
interface Attributes {
[key: string]: string | number | boolean
}
function hyphenate(source: Attributes) {
const result = {}
for (const key in source) {
result[key.replace(/[A-Z]/g, str => '-' + str.toLowerCase())] = source[key]
}
return result
}
function escapeHtml(source: string) {
return source
.replace(/&/g, '&')
.replace(/"/g, '"')
.replace(/'/g, ''')
.replace(/</g, '<')
.replace(/>/g, '>')
}
export class Tag {
public parent: Tag
private children: Tag[] = []
private attributes: Attributes = {}
private innerText: string = ''
constructor(public tag: string) { }
child(tag: string) {
const child = new Tag(tag)
child.parent = this
this.children.push(child)
return child
}
attr(attributes: Attributes) {
this.attributes = {
...this.attributes,
...attributes,
}
return this
}
data(innerText: string) {
this.innerText = innerText
return this
}
line(x1: number, y1: number, x2: number, y2: number, attr: Attributes = {}) {
this.child('line').attr({ ...hyphenate(attr), x1, y1, x2, y2 })
return this
}
circle(cx: number, cy: number, r: number, attr: Attributes = {}) {
this.child('circle').attr({ ...hyphenate(attr), cx, cy, r })
return this
}
rect(x1: number, y1: number, x2: number, y2: number, attr: Attributes = {}) {
this.child('rect').attr({ ...hyphenate(attr), x: x1, y: y1, width: y2 - y1, height: x2 - x1 })
return this
}
text(text: string, x: number, y: number, attr: Attributes = {}) {
this.child('text').attr({ ...hyphenate(attr), x, y }).data(text)
return this
}
g(attr: Attributes = {}) {
return this.child('g').attr(hyphenate(attr))
}
get outer(): string {
const attrText = Object.keys(this.attributes)
.map(key => ` ${key}="${escapeHtml(String(this.attributes[key]))}"`)
.join('')
return `<${this.tag}${attrText}>${this.inner}</${this.tag}>`
}
get inner(): string {
return this.children.length
? this.children.map(child => child.outer).join('')
: this.innerText
}
}
export interface ViewBox {
left?: number
right?: number
top?: number
bottom: number
}
export interface SVGOptions {
size?: number
width?: number
height?: number
magnif?: number
viewBox?: ViewBox
viewSize?: number
}
export class SVG extends Tag {
view: ViewBox
width: number
height: number
constructor(options: SVGOptions = {}) {
super('svg')
const { size = 200, viewSize = size, width = size, height = size } = options
this.width = width
this.height = height
const ratio = viewSize / size
const { left = 0, top = 0, bottom = height * ratio, right = width * ratio } = options.viewBox || {}
this.view = { left, bottom, top, right }
this.attr({
width,
height,
viewBox: `${left} ${top} ${right} ${bottom}`,
xmlns: 'http://www.w3.org/2000/svg',
version: '1.1',
})
}
fill(color: string) {
this.rect(this.view.top, this.view.left, this.view.bottom, this.view.right, { style: `fill: ${color}` })
return this
}
async render(ctx: Context) {
const page = await ctx.puppeteer.page()
await page.setContent(this.outer)
const buffer = await page.screenshot({
clip: {
x: 0,
y: 0,
width: this.width,
height: this.height,
},
})
page.close()
return h.image(buffer, 'image/png')
}
}