接口文档=>ts代码

接口文档=>ts代码,不能完美转化,但是还比较好用

// ==UserScript==
// @name         接口文档=>ts代码
// @namespace    http://tampermonkey.net/
// @version      0.1.11
// @description  接口文档=>ts代码,不能完美转化,但是还比较好用
// @author       fangxianli
// @match        https://km.sankuai.com/*
// @icon         https://www.google.com/s2/favicons?domain=undefined.
// @grant        none
// ==/UserScript==

(function() {
    'use strict';
const InterfaceModel = {
  // 字段名
  names: [/名称/, /参数/, /字段/, /属性/, /名字/, /name/i, /key/i],
  // 类型
  types: [/类型/, /type/i],
  // 含义
  means: [/含义/, /意义/, /意思/, /描述/, /解释/, /说明/, /备注/, /返回/, /mean/i],
  // 例子
  examples: [/示例/, /例子/, /举例/, /eg/i, /example/i]
}

// TODO: 做成可配置的
const TYPE_MAP = [
  {
    name: 'number',
    match: [
      /number/i,
      /int/i,
      /数字/,
      /integer/i,
      /float/i,
      /时间/,
      /date/i,
      /整数/,
      /整型/,
      // TODO: 长整型到底用不用字符串🤔,float 和 double 用字符串不
      /长整型/,
      /long/i,
      /float/i,
      /double/i,
    ]
  },
  {
    name: 'string',
    match: [
      /string/i,
      /字符/,
      /字符串/,
    ]
  },
  {
    name: 'boolean',
    match: [
      /Boolean/i,
      /布尔/,
    ]
  },
  {
    name: 'array',
    match: [
      /list<(\w+)>/i
    ]
  },
  {
    name: 'object',
    match: [
      /(\w+)/
    ]
  }
]

/**
 * 获取之前最邻近的文本内容
 * @param {Element} $currentNode
 */
const findPrevText = ($currentNode) => {
  if (!$currentNode) return
  if ($currentNode?.previousElementSibling?.innerText) return $currentNode?.previousElementSibling?.innerText
  return findPrevText($currentNode.parentElement)
}

/**
 * 深度优先遍历节点,获取树上最深的第一个文本节点
 * @param {Element} node
 */
const dfsFindText = (node) => {
  if (!node?.innerText) return
  if (!node?.children.length) return node.innerText
  for (let i = 0; i < node.children.length; i++) {
    const text = dfsFindText(node.children[i])
    if (text !== undefined) return text
  }
}

/**
 *
 * @param {Element} element
 */
const getContent = (element) => {
  if (!element) return
  const tableInTD = element.querySelectorAll('table')[0]
  if (tableInTD) return findPrevText(tableInTD)
  return element?.innerText?.trim()
}

const testInRegExpArray = (testStr, regExpArr = []) => regExpArr.some(regExp => regExp.test(testStr))

/**
 * 从表头里获取对应字段的位置
 * @param {String} name 字段名
 * @param {Element[]} headers 表头
 */
const getPropsIndex = (name, headers) => headers.findIndex(header => testInRegExpArray(header.innerText, InterfaceModel[name]))

/**
 * 将 table 中的数据转化为能够被识别的类型
 * @param {Element} table
 */
const convertTable2Map = (table) => {
  const prevText = findPrevText(table) || ''
  const charStringArray = prevText?.match(/\w+/g)
  const interfaceName = charStringArray ? charStringArray.join('-') : 'UnknownInterface'
  const typeModel = {
    name: interfaceName,
    defineList: [],
    table
  }
  const headers = [...table.querySelectorAll('th')]
  const nameIndex = getPropsIndex('names', headers)
  const typeIndex = getPropsIndex('types', headers)
  const meanIndex = getPropsIndex('means', headers)
  const exampleIndex = getPropsIndex('examples', headers)
  // 找到第一个 display 不为 none 的 body,即为数据表
  const targetTable = [...table.querySelectorAll('tbody')].find(tbody => tbody.style.display !== 'none')
  // 去除表头
  const rows = [...targetTable.children]
  rows.splice(0, 1)
  const defineList = rows.map((row) => {
    return {
    name: getContent(row.children[nameIndex]),
    type: getContent(row.children[typeIndex]),
    mean: getContent(row.children[meanIndex]),
    example: getContent(row.children[exampleIndex]),
  }})

  typeModel.defineList = defineList
  return typeModel
}

const addCommentPrefix = str => ` * ${str}`
const filterEmpty = arr => arr?.filter(x => x)

const preprocessCode2Arr = str => filterEmpty(str?.split('\n') || [])

const getComment = ({mean, example}) => {
  if (!mean && !example) return []
  const meanArr = preprocessCode2Arr(mean).map(addCommentPrefix) || []
  const exampleArr = preprocessCode2Arr(example).map(addCommentPrefix) || []
  if (exampleArr.length) {
    exampleArr.unshift(' * @example', ' * 示例:')
  }
  const comment = [
    `/**`,
    ...meanArr,
    ...exampleArr,
    ` */`
  ]
  return comment
}

const getTSTypeStr = (type) => {
  if (!type) return 'unknown'
  const tsTypeDesc = TYPE_MAP.find(typeMap => {
    return testInRegExpArray(type, typeMap.match)
  })
  const directReturnArr = ['number', 'string', 'boolean']
  const canDirectReturnName = directReturnArr.includes(tsTypeDesc?.name)
  const needMatch = tsTypeDesc?.name === 'array' || tsTypeDesc?.name === 'object'
  if (canDirectReturnName) return tsTypeDesc?.name
  if (needMatch) {
    for (const match of tsTypeDesc.match) {
      const matchResult = type.match(match)
      if (!matchResult?.[1]) continue
      if (tsTypeDesc.name === 'array') return matchResult[1] + '[]'
      return matchResult[1]
    }
  }
  return 'unknown'
}

const getNameDefine = ({name, type}) => {
  const typeStr = getTSTypeStr(type)
  return [`${name}: ${typeStr}`]
}

const addTab = str => `  ${str}`

const convertData2Code = ({name, defineList}) => {
  const getCode = (defineItem) => {
    return [...getComment(defineItem), ...getNameDefine(defineItem)]
  }

  const interfaceDefine = [
    `interface ${name} {`,
    ...defineList.map(getCode).flat(Infinity).map(addTab),
    `}`,
    ''
  ]
  return interfaceDefine
}

const getCode = () => {
  const allTables = document.querySelectorAll('table')
  /**
   * 筛选出接口定义的table
   */
  const defineTable = [...allTables].filter(table => {
    const headers = [...(table.querySelector('tr')?.children || [])]
    const tableRows = table.querySelectorAll('tr')
    const hasNameTableHeader = headers.some(header => testInRegExpArray(header.innerText, InterfaceModel.names))
    return hasNameTableHeader && tableRows.length > 1
  })

  const defineDataList = defineTable.map(convertTable2Map)
  console.log({defineDataList})
  const codeArray = defineDataList.map(convertData2Code)
  console.log(codeArray)
  return codeArray
}

const copyCode = () => {
  const codeArray = getCode()
  const textArea = document.createElement('textarea')
  textArea.value = codeArray.flat(Infinity).join('\n')
  document.body.append(textArea)
  textArea.select()
  document.execCommand('copy')
  // document.body.remove(textArea)
  textArea.style.display = 'none'
}

const logShortKey = () => console.log("%c  试试快捷键[ ⇧ + ⌃ + ⌥ + j] (shift + ctrl + option + j) 将接口文档转化成 ts 代码吧",
  `color: #333;
  font-size: 16px;
  background-image: linear-gradient(to right, #4facfe, #00f2fe);
  padding: 4px;
  border-radius: 20px;`
)

const logFeedback = () => console.log("%c  使用中遇到问题请点击右侧链接联系: https://x.sankuai.com/bridge/chat?uid=1833639275",
  `color: #333;
  font-size: 16px;
  background-image: linear-gradient(to right, #4facfe, #00f2fe);
  padding: 4px;
  border-radius: 20px;`
)

setTimeout(() => {
  logShortKey()
  logFeedback()
}, 1000)

document.addEventListener('keypress', (event) => {
  if (event.code === 'KeyJ' && event.shiftKey && event.altKey && event.ctrlKey) {
    try {
      copyCode()
      alert('转换代码复制成功,快去粘贴吧')
    } catch (err) {
      console.error(err)
    }
    logFeedback()
  }
})

// Your code here...
})();