You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
OneAuth/oa/front/v.js

336 lines
9.1 KiB
JavaScript

/*
* v.js
* Copyright (C) 2024 veypi <i@veypi.com>
*
* Distributed under terms of the GPL license.
*/
import { proxy } from './vyes.js'
(function() {
'use strict';
const config = { childList: true, subtree: true };
/**
* Represents a book.
* @param {Array} mutationsList
* @param {MutationObserver} observer
*/
const callback = function(mutationsList, observer) {
for (let mutation of mutationsList) {
if (mutation.type === 'childList') {
// console.log(mutation)
// console.log('A child node has been added or removed.');
}
}
};
const observer = new MutationObserver(callback);
var cacheUrl = {}
var pendingRequests = {};
async function fetchFileWithCache(url) {
if (cacheUrl[url]) {
return Promise.resolve(cacheUrl[url]);
}
if (pendingRequests[url]) {
return pendingRequests[url];
}
const promise = fetch(url)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.text();
})
.then(txt => {
cacheUrl[url] = txt;
return txt;
})
.catch(error => {
// console.error('Error fetching the file:', error);
delete pendingRequests[url];
throw error;
})
.finally(() => {
delete pendingRequests[url];
});
pendingRequests[url] = promise;
return promise;
}
function extractDoubleBraceContents(str) {
const regex = /{{|}}|{for{|{end{/g;
let depth = 0;
let currentIndex = 0
let results = [];
let match;
let flag = ''
while ((match = regex.exec(str)) !== null) {
if (match[0] === '{{') {
if (depth === 0) {
// 开始一个新的匹配
results.push([flag, str.slice(currentIndex, match.index)]);
currentIndex = match.index + 2
flag = 'val'
}
depth++;
} else if (match[0] === '}}') {
depth--;
if (depth === 0) {
results.push([flag, str.slice(currentIndex, match.index)]);
currentIndex = match.index + 2
flag = ''
}
} else if (match[0] === '{for{') {
if (depth === 0) {
// 开始一个新的匹配
results.push([flag, str.slice(currentIndex, match.index)]);
currentIndex = match.index + 5
flag = 'for'
}
depth++;
} else if (match[0] === '{end{') {
if (depth === 0) {
// 开始一个新的匹配
results.push([flag, str.slice(currentIndex, match.index)]);
currentIndex = match.index + 5
flag = 'end'
}
depth++;
}
}
results.push([flag, str.slice(currentIndex)]);
return results;
}
function extractBodyAndScript(htmlString) {
const bodyMatch = htmlString.match(/<body[^>]*>([\s\S]*)<\/body>/i);
const bodyContent = bodyMatch ? bodyMatch[1] : '';
const bodyItems = extractDoubleBraceContents(bodyContent);
let scriptMatches = htmlString.match(/<script[^>]*>([\s\S]*?)<\/script>/ig) || []
let scriptContents = []
let srcReg = /<script\s+[^>]*src=["']([^"']+)["'][^>]*>/
for (let s of scriptMatches) {
let match = srcReg.exec(s)
if (match && match.length) {
// handle src script
// console.log(match[1])
} else {
scriptContents.push(s.replace(/<script[^>]*>|<\/script>/ig, ''))
}
}
// 提取 <style> 内容
const styleRegex = /<style[^>]*>([\s\S]*?)<\/style>/gi;
const styleMatches = [...htmlString.matchAll(styleRegex)];
const styleContents = styleMatches.map(match => match[1].trim());
// 解析 <style> 内容,按类名存储
//
const stylesBySelector = {};
if (styleContents.length > 0) {
styleContents.forEach(styleContent => {
const ruleRegex = /(@[\w-]+|[^{]+)\s*\{([\s\S]*?)\}/g;
let ruleMatch;
while ((ruleMatch = ruleRegex.exec(styleContent)) !== null) {
const selector = ruleMatch[1].trim();
const declarations = ruleMatch[2].trim();
if (selector.startsWith('@')) {
// 处理 @keyframes 等特殊规则
const keyframesName = selector.split(' ')[1];
stylesBySelector[keyframesName] = declarations;
} else {
// 处理普通选择器
stylesBySelector[selector] = declarations;
}
}
});
}
return {
bodyContent,
bodyItems,
scriptContents
};
}
/**
* @param{HTMLElement} targetDiv
* @param{HTMLElement} tmplDom
* */
function replaceDom(targetDiv, tmplDom) {
for (var i = 0; i < targetDiv.attributes.length; i++) {
var attr = targetDiv.attributes[i];
if (attr.name === 'class') {
tmplDom.className = tmplDom.className + ' ' + attr.value
} else {
tmplDom.setAttribute(attr.name, attr.value);
}
}
var parent = targetDiv.parentNode;
if (parent) {
parent.replaceChild(tmplDom, targetDiv);
}
}
/**
* @param{string} code
* @param{Object} data
* **/
function run(code, data) {
let args = '{' + Object.keys(data).join(',') + '}'
if (code.indexOf('\n') === -1) {
code = 'return ' + code
}
return new Function(args, code)(data)
}
/**
* @param{HTMLElement} node
* */
async function setupRef(node, data, itercount = 0) {
if (itercount > 100) {
log.warn('infinite loop')
return
}
const ref = node.getAttribute('ref') || ''
node.setAttribute('tag', ref)
node.removeAttribute('ref')
if (!ref) {
return
}
if (ref.endsWith('.js')) {
let mod = await import(ref)
let div = document.createElement('div')
replaceDom(node, mod.default)
node = mod.default
if (typeof mod.setup == 'function') {
let resp = mod.setup(node, data)
}
return
} else {
const txt = await fetchFileWithCache(ref + ".html")
proxy.DomListen(node, () => {
handleHtml(txt, node, data)
})
// let resp = new Function("{vnode,data}", ast.scriptContents.join('\n'))({ vnode: node, data: data })
}
for (let n of node.querySelectorAll("div[ref]")) {
// 确保子元素先加载完
// await setupRef(n, resp?.data || {}, itercount + 1)
}
}
function handleHtml(txt, node, data) {
let ast = extractBodyAndScript(txt)
let resp = new Function("", ast.scriptContents.join('\n'))()
if (resp && typeof resp.data == 'function') {
data = proxy.Watch(resp.data(data))
}
let bodyContent = ast.bodyContent
if (ast.bodyItems.length > 1) {
bodyContent = ''
console.log(ast.bodyItems)
let dataWrap = []
let tmpData = Object.assign({}, data)
let assign = true
let idx
for (idx = 0; idx < ast.bodyItems.length; idx++) {
let item = ast.bodyItems[idx]
if (!assign) {
assign = true
tmpData = Object.assign({}, data)
dataWrap.forEach((item) => {
if (item[1] >= 0) {
tmpData = Object.assign(tmpData, item[0][item[2]])
}
})
console.log(tmpData)
}
if (item[0] === 'val') {
let valr = run(item[1], tmpData)
bodyContent += valr
} else if (item[0] === '') {
bodyContent += item[1]
} else if (item[0] == 'for') {
dataWrap.push([run(item[1], tmpData), idx, 0])
assign = false
} else if (item[0] == 'end') {
let tdw = dataWrap[dataWrap.length - 1]
if (tdw[1] >= 0) {
if (tdw[2] < tdw[0].length - 1) {
tdw[2]++
idx = tdw[1]
} else {
dataWrap.pop()
}
}
assign = false
}
}
}
let div = document.createElement('div')
div.innerHTML = bodyContent
handleSlots(node, div)
// node.innerHTML = div.innerHTML
// let div = document.createElement('div')
replaceDom(node, div)
return resp
}
function handleSlots(origin, template) {
// handle slots
let originSnap = { '': [] }
let i = 0
let child
while (child = origin.children.item(i)) {
i++
let slotname = child.getAttribute ? child.getAttribute('slot') || '' : ''
if (slotname !== '') {
child.removeAttribute('slot')
originSnap[slotname] = child
} else {
originSnap[''].push(child)
}
}
let slots = template.querySelectorAll('slot')
slots.forEach((slot) => {
let slotname = slot.getAttribute('name') || ''
let snap = originSnap[slotname]
if (snap && snap.length !== 0) {
if (slotname === '') {
slot.replaceWith(...snap)
} else {
slot.replaceWith(snap)
}
} else {
slot.replaceWith(...slot.childNodes)
}
})
}
document.addEventListener('DOMContentLoaded', () => {
console.log('DOMContentLoaded');
const node = document.getElementById('app');
// const node = document.body
observer.observe(node, config);
const sync = () => {
let test = node.querySelectorAll("div[ref]")
test.forEach(n => setupRef(n, {}))
}
sync()
// setInterval(sync, 10)
});
})();