背景
实习过程中,需要完善文档站中组件库的组件文档,组件文档包括了组件介绍、组件API表格、demo示例等内容。
__dockit__ index.mdx - 组件文档 demo - 组件demo文件夹,包含多个demo apis - 组件API表格文件夹,包含index.ts
|
流程中重复的步骤比较多,但都是基于已有组件里的代码,抽离出需要的内容,按照格式和需要填充在不同的地方;
填充过程中,有部分需要人工明确:
- 组件代码中的props格式处理和传入;
- 组件的路径;
- 组件demo是否传入数据后,能正常运行(能万事大吉,不能得找源码)
- 组件的API表格,对每个字段的解释(常规通用的字段能解读,但有些字段需要结合源码来理解)
基于一套数据,可以自动化生成的:
- mdx文档中,组件的名称、gitlab路径、组件的demo路径、组件的props和default内容;
- demo文件夹中,常规demo的代码;
- 组件API表格的内容
所以通过deepseek的多次问答修正,加上google colab提供的python环境,编写了一套半自动化的代码;
传入人工从组件源码中阅读拿到的信息,多次运行,拿到index.mdx demo和apis的内容;
人工阅读源码,补充apis里对props的解释。
现阶段的代码有挺多情况未考虑并兼容(源码并不是规范化的一套风格;多数是我传入数据的时候小心点来解决的;)
代码也有冗余的内容,此外多数是通过正则处理字符串实现的。
但能用,所有目的达到了,又有什么大问题呢(嘿哈🤭
实现代码
import re
def parse_props_string(input_str): cleaned = re.sub(r'[\n\t]', '', input_str.strip('{} '))
prop_pattern = re.compile( r'\s*(\w+)\s*:\s*' r'(' r'{.*?}(?=\s*,|\s*$)|' r'[^,]+(?=\s*,|\s*$)' r')', re.DOTALL )
props_dict = {} for match in re.finditer(prop_pattern, cleaned): prop_name = match.group(1) prop_value = match.group(2).strip()
if prop_value.startswith('{'): inner = prop_value[1:-1].strip() type_match = re.search(r'type\s*:\s*([^,]+?)\s*(?:,|$)', inner) default_match = re.search(r'default\s*:\s*([^,]+?)\s*(?:,|$)', inner)
prop_data = {} if type_match: prop_data["type"] = type_match.group(1).strip() if default_match: prop_data["default"] = default_match.group(1).strip()
props_dict[prop_name] = prop_data else: props_dict[prop_name] = prop_value.strip()
return props_dict
|
def generate_props_definition(title, props_data): pattern = re.compile(r'definePropType<(.+?)>')
prop_lines = [] for prop_name, prop_def in props_data.items(): type_expr = None if isinstance(prop_def, dict): type_expr = prop_def.get('type', 'any') else: type_expr = prop_def
match = pattern.search(str(type_expr)) if match: ts_type = match.group(1) else: ts_type = type_expr.split('(')[0].strip() if '(' in type_expr else type_expr
prop_lines.append(f" {prop_name}: {ts_type};")
return f"type {title}Props = {{\n" + "\n".join(prop_lines) + "\n};"
|
def generate_types_docs(ts_code): ts_code = ts_code.strip()
exports = re.split(r'\n\s*\n', ts_code)
sections = [] for export in exports: match = re.match( r'export (interface|type|enum) (\w+)(\s*=\s*)?([\s\S]*)', export.strip() ) if not match: continue
type_kind = match.group(1) type_name = match.group(2) equals_sign = match.group(3) or '' type_body = match.group(4).strip()
if type_kind == 'type': full_def = f"export type {type_name} = {type_body}" elif type_kind == 'enum': full_def = f"export enum {type_name} {type_body}" else: full_def = f"export interface {type_name} {type_body}"
section = f"### {type_name}\n\n```ts\n{full_def}\n```" sections.append(section)
return "\n\n".join(sections)
|
def generate_defaults_docs(ts_code): pattern = re.compile( r'export const (\w+)(?::\s*(\w+))?\s*=\s*(\{.*?\});', re.DOTALL )
sections = [] for match in re.finditer(pattern, ts_code): const_name = match.group(1) const_type = match.group(2) const_value = match.group(3).strip()
if const_type: full_definition = f"export const {const_name}: {const_type} = {const_value};" else: full_definition = f"export const {const_name} = {const_value};"
section = f"### {const_name}\n\n```ts\n{full_definition}\n```" sections.append(section)
return "\n\n".join(sections)
|
def generate_document(params): def extract_gitlab_text(path): components_index = path.find('components/') if components_index != -1: sub_path = path[components_index:] last_slash = sub_path.rfind('/') if last_slash != -1: return sub_path[:last_slash] return ""
gitlab_text = extract_gitlab_text(relative_path)
gitlab_base = "打码---------blob/master/" gitlab_link = gitlab_base + relative_path
file_address = f"""<FileAddress filePath='{relative_path}' gitlab="""
file_address +="{{\ntext:' "+gitlab_text + "',\nlink: '" + gitlab_link + "' \n}}/>"
doc = f"""import {{ APITable }} from 'docSrc/components/table'; import {{ FileAddress }} from 'docSrc/components/file-address'; import {{ data, config }} from './apis/index.ts';
# {title}
! {component_name}
!```tsx !import {{ {title} }} from '--打码---'; !``` ! !{file_address} ! !## 使用示例 ! !<demo ! title="常规" ! desc="{title}使用方法展示" ! src="./packages/brand-biz/src/{gitlab_text}/__dockit__/demos/demo" !/> ! !## API ! !### Props ! !<APITable data={{data}} /> ! !### Config ! !<APITable data={{config}} /> ! !## Types ! ! !### {title}Props ! !```tsx !{props_definition} !``` ! !{ type_definition} ! !## Default ! !{default_definiton} ! !""" ! return doc
|
def generate_component_code(title): return f"""import {{ {title} }} from "打码"
const Demo = () => {{ return ( <FormContainer> <{title}.Component /> </FormContainer> ); }};
export default Demo;"""
import re from typing import Dict, Optional
def parse_params_to_data(props_definition: str, type_definition: str, title: str) -> str: desc_map = { 'configKeyId': '配置的KeyId', 'fieldPathMap': '组件字段映射配置', 'validator': '组件校验配置', 'config': '组件配置项', 'showErrorConfig': '错误信息配置', 'showErrConfigMini': '错误信息AuditInfoBanner的miniSize配置', 'componentKey': 'KeyField属性配置', 'imagesOptions': '图片素材配置', 'uploadImageService': '上传图片服务', 'uploadImage': '上传图片服务', 'title': '标题', 'hasSearchWords': '有搜索词', 'videoDuration': '视频时长', 'disabled': '是否禁用' }
data = [] config = []
if props_definition: props_match = re.search(r'type \w+Props\s*=\s*({.*?});', props_definition, re.DOTALL) if props_match: props_content = props_match.group(1) prop_items = re.finditer(r'(\w+)\s*:\s*([^;\n]+);', props_content)
for match in prop_items: name = match.group(1) type_ = match.group(2).strip() type_ = type_.replace("InteractCreative", title) desc = desc_map.get(name, '')
data.append({ 'name': name, 'desc': desc, 'default': 'undefined', 'type': [type_] })
if type_definition: config_match = re.search(r'### \w+Config.*?```ts.*?export (?:interface|type) \w+Config (.*?```)', type_definition, re.DOTALL) if config_match: config_content = config_match.group(1) config_items = re.finditer(r'(\w+)\??\s*:\s*([^;\n]+);', config_content)
for match in config_items: name = match.group(1) type_ = match.group(2).strip() type_ = type_.replace("InteractCreative", title) desc = desc_map.get(name, '')
config.append({ 'name': name, 'desc': desc, 'default': 'undefined', 'type': [type_] })
output = f"export const data = {format_array(data)};\n\nexport const config = {format_array(config)};" return output
def format_array(arr: list) -> str: """格式化数组为JavaScript数组字符串""" if not arr: return "[]"
items = [] for item in arr: props = [] for k, v in item.items(): if isinstance(v, list): props.append(f"'{k}': {v}") elif isinstance(v, str): props.append(f"'{k}': '{v}'") else: props.append(f"'{k}': {v}") items.append("{\n " + ",\n ".join(props) + "\n }") return "[\n " + ",\n ".join(items) + "\n]"
|
效果
params = { "title": 'SplashVideoFullScreen', "name": "视频全屏 组件", "relativePath": "(-------------打-----码----------------)", "props":""" { fieldPathMap: definePropType<FieldPathMap>(Object), config: definePropType<SplashVideoFullScreenConfig>(Object), showErrorConfig: definePropType<any>(Object), validator: definePropType<any>(Object) } """, "types": """ export type SplashVideoFullScreenConfig = { uploadService: UploadImageService; videoServices: VideoUploadService; imageTitle?: string; imageHint?: string; videoTitle?: string; videoHint?: string; } """, "defaults": """ export const defaultFieldMap = { videoFullScreen: { path: 'materialComponent.videoFullScreen', key: 'VideoFullScreen', childPath: { imageInfoBkList: { path: 'imageInfoBkList' }, videoList: { path: 'videoList' } } } };
export const baseConfig = { imageTitle: '静态底图', imageHint: '要求:文件<300K,支持PNG、JPG、JPEG格式,请分别上传以上分辨率图', videoTitle: '视频内容', videoHint: '要求:时长=5s,文件≤5M,支持MP4格式,请分别上传以上分辨率视频' }; """ }
|
title = params.get('title', '') component_name = params.get('name', '') relative_path = params.get('relativePath', '') props = params.get('props', '') types_config = params.get('types', '') default_config = params.get('defaults', '') props_definition = generate_props_definition(title, parse_props_string(props)) type_definition = generate_types_docs(types_config) default_definiton = generate_defaults_docs(default_config)
print(generate_document(params))
|
title = params.get('title','') print(generate_component_code(title))
|
print(parse_params_to_data(props_definition, type_definition,params.get("title",'')))
|
