服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
1355 lines
39 KiB
Go
1355 lines
39 KiB
Go
package script
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/dop251/goja"
|
|
"github.com/dop251/goja/parser"
|
|
|
|
"pvfine/internal/pvf"
|
|
)
|
|
|
|
// GojaRuntime executes the restricted PVF script language. A runtime is
|
|
// created for each run because Goja runtimes are single-goroutine objects.
|
|
type GojaRuntime struct{}
|
|
|
|
func NewGojaRuntime() *GojaRuntime { return &GojaRuntime{} }
|
|
|
|
// Compile validates syntax without touching an archive.
|
|
func (r *GojaRuntime) Compile(source string) CompileResult {
|
|
if _, err := goja.Compile("pvfine-script.pvf.js", source, false); err != nil {
|
|
return CompileResult{Diagnostics: parseDiagnostics(err)}
|
|
}
|
|
return CompileResult{Valid: true, Diagnostics: []Diagnostic{}}
|
|
}
|
|
|
|
// Run compiles and executes one script in a fresh sandbox.
|
|
func (r *GojaRuntime) Run(ctx context.Context, source string, host *BatchAPI) (RunResult, error) {
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
if host == nil {
|
|
return RunResult{}, fmt.Errorf("脚本宿主为空")
|
|
}
|
|
if err := ctx.Err(); err != nil {
|
|
return cancelledRunResult(err), nil
|
|
}
|
|
program, err := goja.Compile("pvfine-script.pvf.js", source, false)
|
|
if err != nil {
|
|
diagnostics := parseDiagnostics(err)
|
|
result := RunResult{Status: RunStatusFailed}
|
|
if len(diagnostics) > 0 {
|
|
result.Error = &diagnostics[0]
|
|
} else {
|
|
result.Error = &Diagnostic{Kind: ErrorKindCompile, Message: err.Error()}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
vm := goja.New()
|
|
if err := bindRuntime(vm, host); err != nil {
|
|
return RunResult{}, err
|
|
}
|
|
for _, name := range []string{"require", "process", "fetch", "Deno", "WebAssembly"} {
|
|
if err := vm.Set(name, goja.Undefined()); err != nil {
|
|
return RunResult{}, err
|
|
}
|
|
}
|
|
|
|
interruptDone := make(chan struct{})
|
|
go func() {
|
|
select {
|
|
case <-ctx.Done():
|
|
vm.Interrupt(ctx.Err())
|
|
case <-interruptDone:
|
|
}
|
|
}()
|
|
_, runErr := vm.RunProgram(program)
|
|
close(interruptDone)
|
|
|
|
result := RunResult{
|
|
Status: RunStatusCompleted,
|
|
ScannedFiles: host.ScannedCount(),
|
|
ModifiedFiles: host.ModifiedCount(),
|
|
}
|
|
if runErr != nil {
|
|
if contextErr := ctx.Err(); contextErr != nil {
|
|
return cancelledRunResult(contextErr), nil
|
|
}
|
|
diagnostic := runtimeDiagnostic(runErr)
|
|
result.Status = RunStatusFailed
|
|
result.Error = &diagnostic
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func cancelledRunResult(err error) RunResult {
|
|
kind := ErrorKindCancelled
|
|
if errors.Is(err, context.DeadlineExceeded) {
|
|
kind = ErrorKindTimeout
|
|
}
|
|
return RunResult{
|
|
Status: RunStatusCancelled,
|
|
Error: &Diagnostic{Kind: kind, Message: err.Error()},
|
|
}
|
|
}
|
|
|
|
func parseDiagnostics(err error) []Diagnostic {
|
|
result := make([]Diagnostic, 0, 1)
|
|
var list parser.ErrorList
|
|
if errors.As(err, &list) {
|
|
for _, item := range list {
|
|
if item == nil {
|
|
continue
|
|
}
|
|
result = append(result, Diagnostic{
|
|
Kind: ErrorKindCompile,
|
|
Message: item.Message,
|
|
Line: item.Position.Line,
|
|
Column: item.Position.Column,
|
|
})
|
|
}
|
|
return result
|
|
}
|
|
var syntax *goja.CompilerSyntaxError
|
|
if errors.As(err, &syntax) && syntax != nil {
|
|
diagnostic := Diagnostic{Kind: ErrorKindCompile, Message: syntax.Message}
|
|
if syntax.File != nil {
|
|
position := syntax.File.Position(syntax.Offset)
|
|
diagnostic.Line = position.Line
|
|
diagnostic.Column = position.Column
|
|
} else {
|
|
diagnostic.Line, diagnostic.Column = compilerMessagePosition(syntax.Message)
|
|
}
|
|
return []Diagnostic{diagnostic}
|
|
}
|
|
var item *parser.Error
|
|
if errors.As(err, &item) && item != nil {
|
|
return []Diagnostic{{
|
|
Kind: ErrorKindCompile,
|
|
Message: item.Message,
|
|
Line: item.Position.Line,
|
|
Column: item.Position.Column,
|
|
}}
|
|
}
|
|
return []Diagnostic{{Kind: ErrorKindCompile, Message: err.Error()}}
|
|
}
|
|
|
|
func compilerMessagePosition(message string) (int, int) {
|
|
position := strings.Index(message, "Line ")
|
|
if position < 0 {
|
|
return 0, 0
|
|
}
|
|
var line, column int
|
|
if _, err := fmt.Sscanf(message[position:], "Line %d:%d", &line, &column); err != nil {
|
|
return 0, 0
|
|
}
|
|
return line, column
|
|
}
|
|
|
|
func runtimeDiagnostic(err error) Diagnostic {
|
|
diagnostic := Diagnostic{Kind: ErrorKindRuntime, Message: err.Error()}
|
|
if errors.Unwrap(err) != nil {
|
|
diagnostic.Kind = ErrorKindHost
|
|
}
|
|
var exception *goja.Exception
|
|
if errors.As(err, &exception) && exception != nil {
|
|
diagnostic.Message = exception.Error()
|
|
diagnostic.Stack = exception.String()
|
|
frames := exception.Stack()
|
|
if len(frames) > 0 {
|
|
position := frames[0].Position()
|
|
diagnostic.Line = position.Line
|
|
diagnostic.Column = position.Column
|
|
}
|
|
}
|
|
return diagnostic
|
|
}
|
|
|
|
type gojaBindings struct {
|
|
vm *goja.Runtime
|
|
host *BatchAPI
|
|
|
|
files map[*goja.Object]*FileHandle
|
|
documents map[*goja.Object]*documentBinding
|
|
sections map[*goja.Object]*sectionBinding
|
|
}
|
|
|
|
type documentBinding struct {
|
|
host *BatchAPI
|
|
file *FileHandle
|
|
document *pvf.ScriptDocument
|
|
}
|
|
|
|
type sectionBinding struct {
|
|
host *BatchAPI
|
|
document *documentBinding
|
|
section *pvf.ScriptSection
|
|
}
|
|
|
|
func bindRuntime(vm *goja.Runtime, host *BatchAPI) error {
|
|
binding := &gojaBindings{
|
|
vm: vm,
|
|
host: host,
|
|
files: make(map[*goja.Object]*FileHandle),
|
|
documents: make(map[*goja.Object]*documentBinding),
|
|
sections: make(map[*goja.Object]*sectionBinding),
|
|
}
|
|
if err := binding.bindPVF(); err != nil {
|
|
return err
|
|
}
|
|
return binding.bindConsole()
|
|
}
|
|
|
|
func (b *gojaBindings) bindPVF() error {
|
|
pvfObject := b.vm.NewObject()
|
|
if err := setFunction(b.vm, pvfObject, "getOriStrValue", func(call goja.FunctionCall) (goja.Value, error) {
|
|
text, err := requiredString(call.Argument(0), "字符串引用")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resolved, err := b.host.GetOriStrValue(text)
|
|
return b.vm.ToValue(resolved), err
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
if err := setFunction(b.vm, pvfObject, "files", func(goja.FunctionCall) (goja.Value, error) {
|
|
files, err := b.host.Files()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
items := make([]interface{}, 0, len(files))
|
|
for _, file := range files {
|
|
items = append(items, b.fileObject(file))
|
|
}
|
|
return b.vm.NewArray(items...), nil
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
if err := setFunction(b.vm, pvfObject, "find", func(call goja.FunctionCall) (goja.Value, error) {
|
|
filePath, err := requiredString(call.Argument(0), "路径")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
file, found, err := b.host.Find(filePath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !found {
|
|
return goja.Null(), nil
|
|
}
|
|
return b.fileObject(file), nil
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
if err := setFunction(b.vm, pvfObject, "glob", func(call goja.FunctionCall) (goja.Value, error) {
|
|
pattern, err := requiredString(call.Argument(0), "glob 模式")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
files, err := b.host.Glob(pattern)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
items := make([]interface{}, 0, len(files))
|
|
for _, file := range files {
|
|
items = append(items, b.fileObject(file))
|
|
}
|
|
return b.vm.NewArray(items...), nil
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
if err := setFunction(b.vm, pvfObject, "createFile", func(call goja.FunctionCall) (goja.Value, error) {
|
|
filePath, err := requiredString(call.Argument(0), "路径")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
dataType := int64(pvf.TypeScript)
|
|
if !isMissingValue(call.Argument(1)) {
|
|
dataType, err = requiredInteger(call.Argument(1), "文件类型")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
text := ""
|
|
if !isMissingValue(call.Argument(2)) {
|
|
text, err = requiredString(call.Argument(2), "文本")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
file, err := b.host.CreateFile(filePath, int32(dataType), text)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return b.fileObject(file), nil
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
if err := setFunction(b.vm, pvfObject, "copyFile", func(call goja.FunctionCall) (goja.Value, error) {
|
|
from, err := requiredString(call.Argument(0), "源路径")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
to, err := requiredString(call.Argument(1), "目标路径")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
overwrite := false
|
|
if !isMissingValue(call.Argument(2)) {
|
|
overwrite = call.Argument(2).ToBoolean()
|
|
}
|
|
file, err := b.host.CopyFile(from, to, overwrite)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return b.fileObject(file), nil
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
if err := setFunction(b.vm, pvfObject, "deleteFile", func(call goja.FunctionCall) (goja.Value, error) {
|
|
filePath, err := requiredString(call.Argument(0), "路径")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
removed, err := b.host.DeleteFile(filePath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return b.vm.ToValue(removed), nil
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
if err := setFunction(b.vm, pvfObject, "lst", func(call goja.FunctionCall) (goja.Value, error) {
|
|
filePath, err := requiredString(call.Argument(0), "列表路径")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
list, err := b.host.OpenList(filePath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return b.listObject(list), nil
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
if err := setFunction(b.vm, pvfObject, "fileset", func(call goja.FunctionCall) (goja.Value, error) {
|
|
name, err := requiredString(call.Argument(0), "文件集名称")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
fileSet, found, err := b.host.OpenFileSet(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !found {
|
|
return goja.Null(), nil
|
|
}
|
|
return b.fileSetObject(fileSet), nil
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
if err := setFunction(b.vm, pvfObject, "createFileset", func(call goja.FunctionCall) (goja.Value, error) {
|
|
name, err := requiredString(call.Argument(0), "文件集名称")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
paths, err := optionalPathArray(call.Argument(1))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
fileSet, err := b.host.CreateFileSet(name, paths)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return b.fileSetObject(fileSet), nil
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
if err := setFunction(b.vm, pvfObject, "log", func(call goja.FunctionCall) (goja.Value, error) {
|
|
b.host.Log(LogLevelInfo, formatArguments(call.Arguments))
|
|
return goja.Undefined(), nil
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
if err := setFunction(b.vm, pvfObject, "progress", func(call goja.FunctionCall) (goja.Value, error) {
|
|
done, err := requiredInteger(call.Argument(0), "进度完成数")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
total, err := requiredInteger(call.Argument(1), "进度总数")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
message := ""
|
|
if !goja.IsUndefined(call.Argument(2)) && !goja.IsNull(call.Argument(2)) {
|
|
message, err = requiredString(call.Argument(2), "进度说明")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if err := b.host.Progress(int(done), int(total), message); err != nil {
|
|
return nil, err
|
|
}
|
|
return goja.Undefined(), nil
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
types := b.vm.NewObject()
|
|
if err := defineReadOnly(types, "script", b.vm.ToValue(pvf.TypeScript)); err != nil {
|
|
return err
|
|
}
|
|
if err := defineReadOnly(types, "unicode", b.vm.ToValue(pvf.TypeUnicode)); err != nil {
|
|
return err
|
|
}
|
|
if err := defineReadOnly(pvfObject, "types", types); err != nil {
|
|
return err
|
|
}
|
|
if err := defineGetter(b.vm, pvfObject, "modifiedCount", func() int { return b.host.ModifiedCount() }); err != nil {
|
|
return err
|
|
}
|
|
if err := defineGetter(b.vm, pvfObject, "scannedCount", func() int { return b.host.ScannedCount() }); err != nil {
|
|
return err
|
|
}
|
|
return b.vm.Set("pvf", pvfObject)
|
|
}
|
|
|
|
func (b *gojaBindings) bindConsole() error {
|
|
console := b.vm.NewObject()
|
|
for level, method := range map[string]string{
|
|
LogLevelInfo: "log",
|
|
LogLevelWarn: "warn",
|
|
LogLevelError: "error",
|
|
} {
|
|
selectedLevel := level
|
|
if err := setFunction(b.vm, console, method, func(call goja.FunctionCall) (goja.Value, error) {
|
|
b.host.Log(selectedLevel, formatArguments(call.Arguments))
|
|
return goja.Undefined(), nil
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return b.vm.Set("console", console)
|
|
}
|
|
|
|
func (b *gojaBindings) fileObject(file *FileHandle) *goja.Object {
|
|
object := b.vm.NewObject()
|
|
b.files[object] = file
|
|
_ = defineReadOnly(object, "index", b.vm.ToValue(file.Index()))
|
|
_ = defineReadOnly(object, "path", b.vm.ToValue(file.Path()))
|
|
_ = defineReadOnly(object, "type", b.vm.ToValue(file.Type()))
|
|
_ = defineReadOnly(object, "size", b.vm.ToValue(file.Size()))
|
|
_ = setFunction(b.vm, object, "text", func(goja.FunctionCall) (goja.Value, error) {
|
|
text, err := file.Text()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return b.vm.ToValue(text), nil
|
|
})
|
|
_ = setFunction(b.vm, object, "setText", func(call goja.FunctionCall) (goja.Value, error) {
|
|
text, err := requiredString(call.Argument(0), "文本")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := file.SetText(text); err != nil {
|
|
return nil, err
|
|
}
|
|
return goja.Undefined(), nil
|
|
})
|
|
_ = setFunction(b.vm, object, "editUpgrade", func(call goja.FunctionCall) (goja.Value, error) {
|
|
return b.editUpgrade(file, call)
|
|
})
|
|
_ = setFunction(b.vm, object, "parse", func(goja.FunctionCall) (goja.Value, error) {
|
|
document, err := file.Parse()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return b.documentObject(&documentBinding{host: b.host, file: file, document: document}), nil
|
|
})
|
|
_ = setFunction(b.vm, object, "write", func(call goja.FunctionCall) (goja.Value, error) {
|
|
object, ok := call.Argument(0).(*goja.Object)
|
|
if !ok {
|
|
return nil, fmt.Errorf("write 需要 PVFDocument")
|
|
}
|
|
document := b.documents[object]
|
|
if document == nil || document.host != b.host || document.file == nil || document.file.Path() != file.Path() {
|
|
return nil, fmt.Errorf("文档不属于当前文件或事务")
|
|
}
|
|
if err := file.Write(document.document); err != nil {
|
|
return nil, err
|
|
}
|
|
return goja.Undefined(), nil
|
|
})
|
|
return object
|
|
}
|
|
|
|
func (b *gojaBindings) listObject(list *ListHandle) *goja.Object {
|
|
object := b.vm.NewObject()
|
|
_ = defineReadOnly(object, "path", b.vm.ToValue(list.Path()))
|
|
_ = setFunction(b.vm, object, "get", func(goja.FunctionCall) (goja.Value, error) {
|
|
pairs, err := list.Get()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result := b.vm.NewObject()
|
|
for _, pair := range pairs {
|
|
if err := result.Set(pair.ID, pair.Path); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return result, nil
|
|
})
|
|
_ = setFunction(b.vm, object, "forEach", func(call goja.FunctionCall) (goja.Value, error) {
|
|
callback, ok := goja.AssertFunction(call.Argument(0))
|
|
if !ok {
|
|
return nil, fmt.Errorf("forEach 需要回调函数")
|
|
}
|
|
// Iterate in .lst file order, which get() cannot express because JS
|
|
// objects reorder integer-like keys.
|
|
pairs, err := list.Get()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, pair := range pairs {
|
|
if err := list.api.checkContext(); err != nil {
|
|
return nil, err
|
|
}
|
|
if _, err := callback(
|
|
goja.Undefined(),
|
|
b.vm.ToValue(pair.ID),
|
|
b.vm.ToValue(pair.Path),
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return goja.Undefined(), nil
|
|
})
|
|
_ = setFunction(b.vm, object, "set", func(call goja.FunctionCall) (goja.Value, error) {
|
|
id, err := listIDString(call.Argument(0))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
entryPath, err := requiredString(call.Argument(1), "列表路径")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := list.Set(id, entryPath); err != nil {
|
|
return nil, err
|
|
}
|
|
return goja.Undefined(), nil
|
|
})
|
|
_ = setFunction(b.vm, object, "mset", func(call goja.FunctionCall) (goja.Value, error) {
|
|
source, ok := call.Argument(0).(*goja.Object)
|
|
if !ok {
|
|
return nil, fmt.Errorf("mset 需要 id 到路径的对象")
|
|
}
|
|
keys := source.Keys()
|
|
if len(keys) == 0 {
|
|
return goja.Undefined(), nil
|
|
}
|
|
pairs := make([]pvf.ListPair, 0, len(keys))
|
|
for _, id := range keys {
|
|
entryPath, err := requiredString(source.Get(id), "列表路径")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%s: %w", id, err)
|
|
}
|
|
pairs = append(pairs, pvf.ListPair{ID: id, Path: entryPath})
|
|
}
|
|
if err := list.MSet(pairs); err != nil {
|
|
return nil, err
|
|
}
|
|
return goja.Undefined(), nil
|
|
})
|
|
_ = setFunction(b.vm, object, "unset", func(call goja.FunctionCall) (goja.Value, error) {
|
|
id, err := listIDString(call.Argument(0))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
removed, err := list.Unset(id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return b.vm.ToValue(removed), nil
|
|
})
|
|
_ = setFunction(b.vm, object, "getId", func(call goja.FunctionCall) (goja.Value, error) {
|
|
entryPath, err := requiredString(call.Argument(0), "列表路径")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
id, found, err := list.GetID(entryPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !found {
|
|
return goja.Null(), nil
|
|
}
|
|
return b.vm.ToValue(id), nil
|
|
})
|
|
return object
|
|
}
|
|
|
|
func (b *gojaBindings) fileSetObject(fileSet *FileSetHandle) *goja.Object {
|
|
object := b.vm.NewObject()
|
|
_ = defineReadOnly(object, "name", b.vm.ToValue(fileSet.Name()))
|
|
_ = setFunction(b.vm, object, "getAll", func(goja.FunctionCall) (goja.Value, error) {
|
|
paths, err := fileSet.Get()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
items := make([]interface{}, 0, len(paths))
|
|
for _, path := range paths {
|
|
items = append(items, path)
|
|
}
|
|
return b.vm.NewArray(items...), nil
|
|
})
|
|
_ = setFunction(b.vm, object, "setAll", func(call goja.FunctionCall) (goja.Value, error) {
|
|
paths, err := requiredPathArray(call.Argument(0))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
count, err := fileSet.Set(paths)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return b.vm.ToValue(count), nil
|
|
})
|
|
return object
|
|
}
|
|
|
|
func (b *gojaBindings) documentObject(document *documentBinding) *goja.Object {
|
|
object := b.vm.NewObject()
|
|
b.documents[object] = document
|
|
_ = setFunction(b.vm, object, "sections", func(call goja.FunctionCall) (goja.Value, error) {
|
|
path, err := sectionPath(call.Argument(0))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
sections := document.document.Sections(path)
|
|
items := make([]interface{}, 0, len(sections))
|
|
for _, section := range sections {
|
|
items = append(items, b.sectionObject(§ionBinding{host: b.host, document: document, section: section}))
|
|
}
|
|
return b.vm.NewArray(items...), nil
|
|
})
|
|
_ = setFunction(b.vm, object, "section", func(call goja.FunctionCall) (goja.Value, error) {
|
|
path, err := sectionPath(call.Argument(0))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
occurrence, err := optionalInteger(call.Argument(1), 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
section, found := document.document.Section(path, int(occurrence))
|
|
if !found {
|
|
return goja.Null(), nil
|
|
}
|
|
return b.sectionObject(§ionBinding{host: b.host, document: document, section: section}), nil
|
|
})
|
|
_ = setFunction(b.vm, object, "warnings", func(goja.FunctionCall) (goja.Value, error) {
|
|
warnings := document.document.Warnings()
|
|
items := make([]interface{}, 0, len(warnings))
|
|
for _, warning := range warnings {
|
|
item := b.vm.NewObject()
|
|
_ = defineReadOnly(item, "code", b.vm.ToValue(string(warning.Code)))
|
|
_ = defineReadOnly(item, "message", b.vm.ToValue(warning.Message))
|
|
_ = defineReadOnly(item, "tokenIndex", b.vm.ToValue(warning.TokenIndex))
|
|
_ = defineReadOnly(item, "path", b.pathArray(warning.SectionPath))
|
|
items = append(items, item)
|
|
}
|
|
return b.vm.NewArray(items...), nil
|
|
})
|
|
_ = setFunction(b.vm, object, "get", func(call goja.FunctionCall) (goja.Value, error) {
|
|
value, found, err := b.documentValue(document, call, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !found {
|
|
return goja.Undefined(), nil
|
|
}
|
|
return b.vm.ToValue(value.Value), nil
|
|
})
|
|
_ = setFunction(b.vm, object, "getValue", func(call goja.FunctionCall) (goja.Value, error) {
|
|
value, found, err := b.documentValue(document, call, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !found {
|
|
return goja.Undefined(), nil
|
|
}
|
|
return b.valueObject(value), nil
|
|
})
|
|
_ = setFunction(b.vm, object, "getOriStrValue", func(call goja.FunctionCall) (goja.Value, error) {
|
|
value, found, err := b.documentValue(document, call, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return b.originalStringValue(value, found)
|
|
})
|
|
for _, method := range []string{"set", "setStrValue"} {
|
|
_ = setFunction(b.vm, object, method, func(call goja.FunctionCall) (goja.Value, error) {
|
|
path, err := sectionPath(call.Argument(0))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(path) == 0 {
|
|
return nil, fmt.Errorf("section 路径不能为空")
|
|
}
|
|
options, err := objectArgument(call.Argument(2))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
occurrence, err := optionInteger(options, "occurrence", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
valueIndex, err := optionInteger(options, "valueIndex", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
create, err := optionBool(options, "create", false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
endTag, err := optionBool(options, "endTag", false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var existing *pvf.ScriptValue
|
|
if section, found := document.document.Section(path, int(occurrence)); found {
|
|
if current, found := section.GetValue(int(valueIndex)); found {
|
|
existing = ¤t
|
|
}
|
|
if existing == nil {
|
|
return nil, fmt.Errorf("section 没有对应的直接值")
|
|
}
|
|
} else if !create {
|
|
return b.vm.ToValue(false), nil
|
|
} else if occurrence < 0 || valueIndex != 0 {
|
|
return nil, fmt.Errorf("新建 section 只能设置第 0 个值")
|
|
}
|
|
var value pvf.ScriptValue
|
|
if method == "setStrValue" {
|
|
value, err = b.legacyStringValue(call.Argument(1))
|
|
} else {
|
|
value, err = b.setValue(call.Argument(1), existing, document.file.Path())
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
changed, err := document.document.Set(path, int(occurrence), int(valueIndex), value, create, endTag)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return b.vm.ToValue(changed), nil
|
|
})
|
|
}
|
|
_ = setFunction(b.vm, object, "delete", func(call goja.FunctionCall) (goja.Value, error) {
|
|
path, err := sectionPath(call.Argument(0))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
occurrence, err := optionalInteger(call.Argument(1), 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
changed, err := document.document.Delete(path, int(occurrence))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return b.vm.ToValue(changed), nil
|
|
})
|
|
_ = setFunction(b.vm, object, "appendSection", func(call goja.FunctionCall) (goja.Value, error) {
|
|
name, err := requiredString(call.Argument(0), "section 名称")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
values, err := b.scriptValues(call.Argument(1), nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
options, err := objectArgument(call.Argument(2))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
endTag, err := optionBool(options, "endTag", false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
section, err := document.document.AppendSection(name, values, endTag)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return b.sectionObject(§ionBinding{host: b.host, document: document, section: section}), nil
|
|
})
|
|
return object
|
|
}
|
|
|
|
func (b *gojaBindings) sectionObject(binding *sectionBinding) *goja.Object {
|
|
object := b.vm.NewObject()
|
|
b.sections[object] = binding
|
|
_ = defineReadOnly(object, "name", b.vm.ToValue(binding.section.Name()))
|
|
_ = defineReadOnly(object, "hasEndTag", b.vm.ToValue(binding.section.HasEndTag()))
|
|
_ = defineGetter(b.vm, object, "path", func() *goja.Object { return b.pathArray(binding.section.Path()) })
|
|
_ = defineGetter(b.vm, object, "occurrence", func() int { return binding.section.Occurrence() })
|
|
_ = setFunction(b.vm, object, "values", func(goja.FunctionCall) (goja.Value, error) {
|
|
values, err := b.sectionValues(binding)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return b.vm.NewArray(values...), nil
|
|
})
|
|
_ = setFunction(b.vm, object, "children", func(goja.FunctionCall) (goja.Value, error) {
|
|
if err := b.checkSection(binding); err != nil {
|
|
return nil, err
|
|
}
|
|
children := binding.section.Children()
|
|
items := make([]interface{}, 0, len(children))
|
|
for _, child := range children {
|
|
items = append(items, b.sectionObject(§ionBinding{host: b.host, document: binding.document, section: child}))
|
|
}
|
|
return b.vm.NewArray(items...), nil
|
|
})
|
|
_ = setFunction(b.vm, object, "removeChildren", func(call goja.FunctionCall) (goja.Value, error) {
|
|
if err := b.checkSection(binding); err != nil {
|
|
return nil, err
|
|
}
|
|
name, err := requiredString(call.Argument(0), "子 section 名称")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
count, err := binding.section.RemoveChildren(name)
|
|
return b.vm.ToValue(count), err
|
|
})
|
|
_ = setFunction(b.vm, object, "get", func(call goja.FunctionCall) (goja.Value, error) {
|
|
value, found, err := b.sectionValue(binding, call.Argument(0), false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !found {
|
|
return goja.Undefined(), nil
|
|
}
|
|
return b.vm.ToValue(value.Value), nil
|
|
})
|
|
_ = setFunction(b.vm, object, "getValue", func(call goja.FunctionCall) (goja.Value, error) {
|
|
value, found, err := b.sectionValue(binding, call.Argument(0), false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !found {
|
|
return goja.Undefined(), nil
|
|
}
|
|
return b.valueObject(value), nil
|
|
})
|
|
_ = setFunction(b.vm, object, "getOriStrValue", func(call goja.FunctionCall) (goja.Value, error) {
|
|
value, found, err := b.sectionValue(binding, call.Argument(0), false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return b.originalStringValue(value, found)
|
|
})
|
|
_ = setFunction(b.vm, object, "setStrValue", func(call goja.FunctionCall) (goja.Value, error) {
|
|
if err := b.checkSection(binding); err != nil {
|
|
return nil, err
|
|
}
|
|
value, err := b.legacyStringValue(call.Argument(0))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
index, err := optionalInteger(call.Argument(1), 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return goja.Undefined(), binding.section.Set(value, int(index))
|
|
})
|
|
_ = setFunction(b.vm, object, "set", func(call goja.FunctionCall) (goja.Value, error) {
|
|
if err := b.checkSection(binding); err != nil {
|
|
return nil, err
|
|
}
|
|
index, err := optionalInteger(call.Argument(1), 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var existing *pvf.ScriptValue
|
|
if current, found := binding.section.GetValue(int(index)); found {
|
|
existing = ¤t
|
|
}
|
|
if existing == nil {
|
|
return nil, fmt.Errorf("section 没有对应的直接值")
|
|
}
|
|
value, err := b.setValue(call.Argument(0), existing, binding.document.file.Path())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := binding.section.Set(value, int(index)); err != nil {
|
|
return nil, err
|
|
}
|
|
return goja.Undefined(), nil
|
|
})
|
|
_ = setFunction(b.vm, object, "setValues", func(call goja.FunctionCall) (goja.Value, error) {
|
|
if err := b.checkSection(binding); err != nil {
|
|
return nil, err
|
|
}
|
|
items, err := arrayArguments(call.Argument(0))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
current := binding.section.Values()
|
|
values := make([]pvf.ScriptValue, 0, len(items))
|
|
for index, item := range items {
|
|
var existing *pvf.ScriptValue
|
|
if index < len(current) {
|
|
existingValue := current[index]
|
|
existing = &existingValue
|
|
}
|
|
value, err := b.scriptValue(item, existing)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
values = append(values, value)
|
|
}
|
|
if err := binding.section.SetValues(values); err != nil {
|
|
return nil, err
|
|
}
|
|
return goja.Undefined(), nil
|
|
})
|
|
_ = setFunction(b.vm, object, "append", func(call goja.FunctionCall) (goja.Value, error) {
|
|
if err := b.checkSection(binding); err != nil {
|
|
return nil, err
|
|
}
|
|
value, err := b.scriptValue(call.Argument(0), nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := binding.section.Append(value); err != nil {
|
|
return nil, err
|
|
}
|
|
return goja.Undefined(), nil
|
|
})
|
|
_ = setFunction(b.vm, object, "appendSection", func(call goja.FunctionCall) (goja.Value, error) {
|
|
if err := b.checkSection(binding); err != nil {
|
|
return nil, err
|
|
}
|
|
name, err := requiredString(call.Argument(0), "section 名称")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
values, err := b.scriptValues(call.Argument(1), nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
options, err := objectArgument(call.Argument(2))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
endTag, err := optionBool(options, "endTag", false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
section, err := binding.section.AppendSection(name, values, endTag)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return b.sectionObject(§ionBinding{host: b.host, document: binding.document, section: section}), nil
|
|
})
|
|
_ = setFunction(b.vm, object, "delete", func(goja.FunctionCall) (goja.Value, error) {
|
|
if err := b.checkSection(binding); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := binding.section.Delete(); err != nil {
|
|
return nil, err
|
|
}
|
|
return goja.Undefined(), nil
|
|
})
|
|
return object
|
|
}
|
|
|
|
func (b *gojaBindings) documentValue(document *documentBinding, call goja.FunctionCall, exact bool) (pvf.ScriptValue, bool, error) {
|
|
path, err := sectionPath(call.Argument(0))
|
|
if err != nil {
|
|
return pvf.ScriptValue{}, false, err
|
|
}
|
|
occurrence, err := optionalInteger(call.Argument(1), 0)
|
|
if err != nil {
|
|
return pvf.ScriptValue{}, false, err
|
|
}
|
|
valueIndex, err := optionalInteger(call.Argument(2), 0)
|
|
if err != nil {
|
|
return pvf.ScriptValue{}, false, err
|
|
}
|
|
value, found := document.document.GetValue(path, int(occurrence), int(valueIndex))
|
|
return value, found, nil
|
|
}
|
|
|
|
func (b *gojaBindings) sectionValues(binding *sectionBinding) ([]interface{}, error) {
|
|
if err := b.checkSection(binding); err != nil {
|
|
return nil, err
|
|
}
|
|
values := binding.section.Values()
|
|
result := make([]interface{}, 0, len(values))
|
|
for _, value := range values {
|
|
result = append(result, b.valueObject(value))
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (b *gojaBindings) sectionValue(binding *sectionBinding, value goja.Value, _ bool) (pvf.ScriptValue, bool, error) {
|
|
if err := b.checkSection(binding); err != nil {
|
|
return pvf.ScriptValue{}, false, err
|
|
}
|
|
index, err := optionalInteger(value, 0)
|
|
if err != nil {
|
|
return pvf.ScriptValue{}, false, err
|
|
}
|
|
result, found := binding.section.GetValue(int(index))
|
|
return result, found, nil
|
|
}
|
|
|
|
func (b *gojaBindings) originalStringValue(value pvf.ScriptValue, found bool) (goja.Value, error) {
|
|
if !found {
|
|
return goja.Undefined(), nil
|
|
}
|
|
text, ok := value.Value.(string)
|
|
if !ok {
|
|
return nil, fmt.Errorf("值不是字符串")
|
|
}
|
|
resolved, err := b.host.GetOriStrValue(text)
|
|
return b.vm.ToValue(resolved), err
|
|
}
|
|
|
|
func (b *gojaBindings) legacyStringValue(input goja.Value) (pvf.ScriptValue, error) {
|
|
text, err := requiredString(input, "字符串")
|
|
if err != nil {
|
|
return pvf.ScriptValue{}, err
|
|
}
|
|
return pvf.NewScriptValue(pvf.ScriptTokenQuoted, text, "")
|
|
}
|
|
|
|
func (b *gojaBindings) setValue(input goja.Value, existing *pvf.ScriptValue, owner string) (pvf.ScriptValue, error) {
|
|
if _, object := input.(*goja.Object); !object && !isMissingValue(input) {
|
|
if text, ok := input.Export().(string); ok {
|
|
if b.host.tx.Stage().ContentRules().SupportsStringReferences {
|
|
return b.host.tx.RegisterStringReference(owner, text, existing)
|
|
}
|
|
return b.legacyStringValue(input)
|
|
}
|
|
}
|
|
return b.scriptValue(input, existing)
|
|
}
|
|
|
|
func (b *gojaBindings) valueObject(value pvf.ScriptValue) *goja.Object {
|
|
object := b.vm.NewObject()
|
|
_ = defineReadOnly(object, "type", b.vm.ToValue(string(value.Type)))
|
|
_ = defineReadOnly(object, "value", b.vm.ToValue(value.Value))
|
|
if value.Pool != "" {
|
|
_ = defineReadOnly(object, "pool", b.vm.ToValue(string(value.Pool)))
|
|
}
|
|
return object
|
|
}
|
|
|
|
func (b *gojaBindings) scriptValues(value goja.Value, existing []pvf.ScriptValue) ([]pvf.ScriptValue, error) {
|
|
if isMissingValue(value) {
|
|
return []pvf.ScriptValue{}, nil
|
|
}
|
|
items, err := arrayArguments(value)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result := make([]pvf.ScriptValue, 0, len(items))
|
|
for index, item := range items {
|
|
var current *pvf.ScriptValue
|
|
if index < len(existing) {
|
|
copyValue := existing[index]
|
|
current = ©Value
|
|
}
|
|
converted, err := b.scriptValue(item, current)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result = append(result, converted)
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (b *gojaBindings) scriptValue(value goja.Value, existing *pvf.ScriptValue) (pvf.ScriptValue, error) {
|
|
if object, ok := value.(*goja.Object); ok && object != nil && !goja.IsNull(value) {
|
|
kind, err := requiredString(object.Get("type"), "token 类型")
|
|
if err != nil {
|
|
return pvf.ScriptValue{}, fmt.Errorf("精确值缺少 type: %w", err)
|
|
}
|
|
input, err := exportScalar(object.Get("value"))
|
|
if err != nil {
|
|
return pvf.ScriptValue{}, fmt.Errorf("精确值缺少 value: %w", err)
|
|
}
|
|
pool := pvf.ScriptStringPool("")
|
|
if poolValue := object.Get("pool"); !isMissingValue(poolValue) {
|
|
poolName, poolErr := requiredString(poolValue, "字符串池")
|
|
if poolErr != nil {
|
|
return pvf.ScriptValue{}, poolErr
|
|
}
|
|
pool = pvf.ScriptStringPool(poolName)
|
|
}
|
|
return pvf.NewScriptValue(pvf.ScriptTokenType(kind), input, pool)
|
|
}
|
|
|
|
input, err := exportScalar(value)
|
|
if err != nil {
|
|
return pvf.ScriptValue{}, err
|
|
}
|
|
if existing != nil {
|
|
return pvf.NewScriptValue(existing.Type, input, existing.Pool)
|
|
}
|
|
if text, ok := input.(string); ok {
|
|
return pvf.NewScriptValue(pvf.ScriptTokenQuoted, text, pvf.ScriptPoolUTF8)
|
|
}
|
|
number, ok := input.(float64)
|
|
if !ok || math.IsNaN(number) || math.IsInf(number, 0) {
|
|
return pvf.ScriptValue{}, fmt.Errorf("值必须是 number 或 string")
|
|
}
|
|
if math.Trunc(number) == number && number >= math.MinInt32 && number <= math.MaxInt32 {
|
|
return pvf.NewScriptValue(pvf.ScriptTokenInteger, int64(number), "")
|
|
}
|
|
return pvf.NewScriptValue(pvf.ScriptTokenFloat, number, "")
|
|
}
|
|
|
|
func (b *gojaBindings) checkSection(binding *sectionBinding) error {
|
|
if binding == nil || binding.host != b.host || binding.section == nil || binding.section.Occurrence() < 0 {
|
|
return fmt.Errorf("section 句柄无效")
|
|
}
|
|
return b.host.checkContext()
|
|
}
|
|
|
|
func (b *gojaBindings) pathArray(path []string) *goja.Object {
|
|
items := make([]interface{}, len(path))
|
|
for index, part := range path {
|
|
items[index] = part
|
|
}
|
|
return b.vm.NewArray(items...)
|
|
}
|
|
|
|
func defineReadOnly(object *goja.Object, name string, value goja.Value) error {
|
|
return object.DefineDataProperty(name, value, goja.FLAG_FALSE, goja.FLAG_FALSE, goja.FLAG_TRUE)
|
|
}
|
|
|
|
func setFunction(vm *goja.Runtime, object *goja.Object, name string, fn func(goja.FunctionCall) (goja.Value, error)) error {
|
|
return object.Set(name, func(call goja.FunctionCall) goja.Value {
|
|
value, err := fn(call)
|
|
if err != nil {
|
|
panic(vm.NewGoError(err))
|
|
}
|
|
return value
|
|
})
|
|
}
|
|
|
|
func defineGetter(vm *goja.Runtime, object *goja.Object, name string, getter interface{}) error {
|
|
return object.DefineAccessorProperty(name, vm.ToValue(getter), goja.Undefined(), goja.FLAG_FALSE, goja.FLAG_TRUE)
|
|
}
|
|
|
|
func requiredString(value goja.Value, label string) (string, error) {
|
|
if isMissingValue(value) {
|
|
return "", fmt.Errorf("%s不能为空", label)
|
|
}
|
|
text, ok := value.Export().(string)
|
|
if !ok {
|
|
return "", fmt.Errorf("%s必须是字符串", label)
|
|
}
|
|
return text, nil
|
|
}
|
|
|
|
// listIDString reads a .lst id, which is commonly written as a number in JSON
|
|
// while the token stream stores it as a plain integer.
|
|
func listIDString(value goja.Value) (string, error) {
|
|
if isMissingValue(value) {
|
|
return "", fmt.Errorf("列表 id 不能为空")
|
|
}
|
|
switch exported := value.Export().(type) {
|
|
case string:
|
|
return exported, nil
|
|
case int64:
|
|
return strconv.FormatInt(exported, 10), nil
|
|
case int:
|
|
return strconv.FormatInt(int64(exported), 10), nil
|
|
case float64:
|
|
if math.Trunc(exported) != exported {
|
|
return "", fmt.Errorf("列表 id 必须是整数或字符串")
|
|
}
|
|
return strconv.FormatInt(int64(exported), 10), nil
|
|
default:
|
|
return "", fmt.Errorf("列表 id 必须是整数或字符串")
|
|
}
|
|
}
|
|
|
|
func exportScalar(value goja.Value) (any, error) {
|
|
if isMissingValue(value) {
|
|
return nil, fmt.Errorf("值不能是 null 或 undefined")
|
|
}
|
|
exported := value.Export()
|
|
switch scalar := exported.(type) {
|
|
case string:
|
|
return scalar, nil
|
|
case int:
|
|
return float64(scalar), nil
|
|
case int64:
|
|
return float64(scalar), nil
|
|
case float64:
|
|
return scalar, nil
|
|
case float32:
|
|
return float64(scalar), nil
|
|
default:
|
|
return nil, fmt.Errorf("值必须是 number 或 string")
|
|
}
|
|
}
|
|
|
|
func requiredInteger(value goja.Value, label string) (int64, error) {
|
|
if isMissingValue(value) {
|
|
return 0, fmt.Errorf("%s不能为空", label)
|
|
}
|
|
number := value.ToFloat()
|
|
if math.IsNaN(number) || math.IsInf(number, 0) || math.Trunc(number) != number {
|
|
return 0, fmt.Errorf("%s必须是整数", label)
|
|
}
|
|
return int64(number), nil
|
|
}
|
|
|
|
func optionalInteger(value goja.Value, fallback int64) (int64, error) {
|
|
if isMissingValue(value) {
|
|
return fallback, nil
|
|
}
|
|
return requiredInteger(value, "下标")
|
|
}
|
|
|
|
func objectArgument(value goja.Value) (*goja.Object, error) {
|
|
if isMissingValue(value) {
|
|
return nil, nil
|
|
}
|
|
object, ok := value.(*goja.Object)
|
|
if !ok {
|
|
return nil, fmt.Errorf("options 必须是对象")
|
|
}
|
|
return object, nil
|
|
}
|
|
|
|
func optionInteger(object *goja.Object, name string, fallback int64) (int64, error) {
|
|
if object == nil {
|
|
return fallback, nil
|
|
}
|
|
return optionalInteger(object.Get(name), fallback)
|
|
}
|
|
|
|
func optionBool(object *goja.Object, name string, fallback bool) (bool, error) {
|
|
if object == nil || isMissingValue(object.Get(name)) {
|
|
return fallback, nil
|
|
}
|
|
return object.Get(name).ToBoolean(), nil
|
|
}
|
|
|
|
func arrayArguments(value goja.Value) ([]goja.Value, error) {
|
|
if isMissingValue(value) {
|
|
return nil, fmt.Errorf("values 必须是数组")
|
|
}
|
|
object, ok := value.(*goja.Object)
|
|
if !ok || object.ClassName() != "Array" {
|
|
return nil, fmt.Errorf("values 必须是数组")
|
|
}
|
|
length, err := requiredInteger(object.Get("length"), "数组长度")
|
|
if err != nil || length < 0 || length > 1_000_000 {
|
|
return nil, fmt.Errorf("数组长度无效")
|
|
}
|
|
result := make([]goja.Value, 0, length)
|
|
for index := int64(0); index < length; index++ {
|
|
result = append(result, object.Get(fmt.Sprintf("%d", index)))
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func sectionPath(value goja.Value) ([]string, error) {
|
|
if isMissingValue(value) {
|
|
return nil, fmt.Errorf("section 路径不能为空")
|
|
}
|
|
if text, ok := value.Export().(string); ok {
|
|
parts := strings.Split(strings.ReplaceAll(text, "\\", "/"), "/")
|
|
result := make([]string, 0, len(parts))
|
|
for _, part := range parts {
|
|
part = strings.TrimSpace(strings.Trim(part, "[]"))
|
|
part = strings.TrimPrefix(part, "/")
|
|
if part != "" {
|
|
result = append(result, part)
|
|
}
|
|
}
|
|
if len(result) == 0 {
|
|
return nil, fmt.Errorf("section 路径不能为空")
|
|
}
|
|
return result, nil
|
|
}
|
|
items, err := arrayArguments(value)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("section 路径必须是字符串或字符串数组")
|
|
}
|
|
result := make([]string, 0, len(items))
|
|
for _, item := range items {
|
|
part, err := requiredString(item, "section 路径项")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
part = strings.TrimSpace(strings.Trim(part, "[]"))
|
|
part = strings.TrimPrefix(part, "/")
|
|
if part != "" {
|
|
result = append(result, part)
|
|
}
|
|
}
|
|
if len(result) == 0 {
|
|
return nil, fmt.Errorf("section 路径不能为空")
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// requiredPathArray reads a mandatory array of path strings.
|
|
func requiredPathArray(value goja.Value) ([]string, error) {
|
|
if isMissingValue(value) {
|
|
return nil, fmt.Errorf("路径列表必须是数组")
|
|
}
|
|
return pathArrayItems(value)
|
|
}
|
|
|
|
// optionalPathArray reads an optional array of path strings; a missing value
|
|
// means an empty list so createFileset(name) can create an empty set.
|
|
func optionalPathArray(value goja.Value) ([]string, error) {
|
|
if isMissingValue(value) {
|
|
return nil, nil
|
|
}
|
|
return pathArrayItems(value)
|
|
}
|
|
|
|
func pathArrayItems(value goja.Value) ([]string, error) {
|
|
items, err := arrayArguments(value)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("路径列表必须是数组")
|
|
}
|
|
paths := make([]string, 0, len(items))
|
|
for index, item := range items {
|
|
path, err := requiredString(item, fmt.Sprintf("路径列表第 %d 项", index+1))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
paths = append(paths, path)
|
|
}
|
|
return paths, nil
|
|
}
|
|
|
|
func isMissingValue(value goja.Value) bool {
|
|
return value == nil || goja.IsUndefined(value) || goja.IsNull(value)
|
|
}
|
|
|
|
func formatArguments(arguments []goja.Value) string {
|
|
parts := make([]string, len(arguments))
|
|
for index, value := range arguments {
|
|
parts[index] = value.String()
|
|
}
|
|
return strings.Join(parts, " ")
|
|
}
|