Greasy Fork

Greasy Fork is available in English.

接口文档=>ts代码

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

当前为 2021-09-13 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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...
})();