// ==UserScript== // @name OceanEngine Table Transformer Kotlin // @namespace http://tampermonkey.net/ // @version 2.0 // @description try to take over the world! // @author You // @match https://open.oceanengine.com/labels/7/docs/* // @grant GM_setClipboard // @run-at document-start // ==/UserScript== (function() { 'use strict'; const makeClassName = s => s?.replace(/(?:_|^)([a-z])/g, g => g.substring(g.length - 1).toUpperCase()) || '' const convertType = (type, name) => { const isList = type.endsWith('[]') if (isList) type = type.substring(0, type.length - 2) switch (type.toLowerCase()) { case 'float': type = 'Float'; break case 'double': type = 'Double'; break case 'number': case 'int': case 'long': case 'integer': type = 'Long'; break case 'boolean': type = 'Boolean'; break case 'string': type = 'String'; break case 'json': case 'object': type = makeClassName(name) || 'Object'; break } if (isList) type = `List<${type}>` return type } const parseName = (childNodes) => { if (!childNodes) return for (const node of childNodes) { if (node.nodeType === Node.TEXT_NODE) { const text = node.textContent.trim() if (text) return text.replace(/\n/g, '') } } } const parseRow = (tds) => { const name = parseName(tds[0].querySelector('div')?.childNodes) if (!name) return '' const optional = tds[0].querySelector('span.tag')?.innerText?.trim() !== '必填' const type = tds[1].innerText.trim() const comment = tds[2].innerText.trim() return ` val ${name}: ${convertType(type, name)}${optional ? '? = null' : ''}, // ${comment.replace(/\n/g, ' ')}\n` } const parseEnumRow = (tds) => { const name = parseName(tds[0].querySelector('div')?.childNodes) if (!name) return '' let safeName = name.toUpperCase().replace(/\n/g, '').replace(/[^A-Z0-9_]/g, '_') if (!/^[A-Z]/.test(safeName)) safeName = `_${safeName}` const comment = tds[1].innerText.trim() return `${name === safeName ? '' : ` @SerializedName("${name}")\n`} ${safeName}, // ${comment.replace(/\n/g, ' ')}\n` } const parseTable = (table, selector, parentSelector, result) => { const getOuterTitle = () => { let node = table.previousElementSibling let title = 'Root' while (node) { if (/^H\d$/.test(node.tagName)) { title = node.innerText.replace(/\s/g, '') break } node = node.previousElementSibling } return title } const boundaries = parentSelector ? Array.from(table.querySelectorAll(parentSelector)).map(node => [parseName(node.querySelector('div.tdData').childNodes)?.replace(/(?:_|^)([a-z])/g, g => g.substring(g.length - 1).toUpperCase()), node]) : [[getOuterTitle(), null]] // [title as string, boundary as Node] console.log(boundaries) const isEnum = table.querySelector('th')?.innerText?.trim() === '值' const closingTag = isEnum ? '}' : ')' let content = '' table.querySelectorAll(selector).forEach(row => { const prevNode = row.previousSibling const boundary = boundaries.find(b => prevNode === b[1]) if (boundary) { if (content) { // not first entity, close current item content += closingTag + '\n' } const title = boundary[0] content += isEnum ? `enum class ${title} {\n` : `data class ${title} (\n` } content += (isEnum ? parseEnumRow : parseRow)(row.querySelectorAll('td')) }) if (content) content += closingTag result.innerText = content result.style.display = '' GM_setClipboard(result.innerText) } const ENUM_HINT = ['允许值', '枚举值'] const hasEnumHit = s => ENUM_HINT.some(hint => s?.includes?.(hint)) const processEnumerationNode = (node) => { if (node.nodeType === Node.ELEMENT_NODE && node.tagName === 'CODE') { if (node.previousSibling?.tagName !== 'BR') node.parentNode.insertBefore(document.createElement('br'), node) node.innerText = node.innerText.replace(/["'\s]/g, '') } else if (node.nodeType === Node.TEXT_NODE && node.previousSibling?.tagName === 'CODE') { const comment = node.textContent.trim().replace(/、|,|,$/g, '') node.textContent = comment ? `,\u00A0//\u00A0${comment}` : ',' } } const formatEnumerations = (table) => { table.querySelectorAll('tr > td:nth-child(3) > div').forEach(div => { if (hasEnumHit(div.innerText)) { // 提示模式 允许值:blahblah let started = false let node = div.childNodes[0] while (node) { if (started) { processEnumerationNode(node) } else if (node.nodeType === Node.TEXT_NODE && hasEnumHit(node.textContent)) { started = true } node = node.nextSibling } } else { // 行首模式 let node = div.querySelector('br + code') if (node?.previousSibling?.tagName !== 'BR') return while (node) { processEnumerationNode(node) node = node.nextSibling } } }) } const createButton = (table) => { if (table.dataset.done) return table.dataset.done = true formatEnumerations(table) const levels = Array.from(table.querySelectorAll('tr')).reduce((s, row) => { if (row.className) s.add(row.className) return s }, new Set()) const container = document.createElement('div') const result = document.createElement('pre') result.style.display = 'none' table.parentNode.insertBefore(container, table) table.parentNode.insertBefore(result, table) const addBtn = (level, parentLevel) => { const btn = document.createElement('button') btn.append(document.createTextNode(level)) btn.addEventListener('click', () => parseTable(table, `tbody > tr.${level}`, parentLevel ? `tbody > tr.${parentLevel}` : undefined, result)) container.append(btn) } Array.from(levels.values()).forEach((lv, i, a) => addBtn(lv, a[i - 1])) } const ob = new MutationObserver(mutations => { mutations.forEach(({ target }) => { if (target?.tagName === 'DIV' && target?.classList?.contains('doc-content-body')) { target.querySelectorAll('table').forEach(createButton) } }) }) ob.observe(document.body, { subtree: true, childList: true }) setTimeout(() => document.querySelectorAll('table').forEach(createButton), 3000) })();