feat: add module outline parser

This commit is contained in:
王鹏
2026-07-07 13:58:28 +08:00
parent 9d7c79aecf
commit 0e3d2f09a2
2 changed files with 239 additions and 0 deletions

View File

@@ -0,0 +1,131 @@
const DEFAULT_OUTLINE_TITLE = '系统模块设计'
const FALLBACK_GROUP_TITLE = '未分组模块'
function cleanText(value, fallback = '') {
const text = String(value ?? '').trim()
return text || fallback
}
function textFromFields(value, fallback = '') {
if (!value || typeof value !== 'object') return cleanText(value, fallback)
return cleanText(value.title ?? value.name ?? value.code, fallback)
}
function menuTitle(menu, index) {
return cleanText(menu?.name ?? menu?.title ?? menu?.code, `功能${index + 1}`)
}
function warning(code, line) {
return line === undefined ? { code } : { code, line }
}
function createGroup(title) {
return {
title,
items: []
}
}
export function normalizeModuleOutline(outline = {}) {
const groups = Array.isArray(outline.groups) ? outline.groups : []
return {
title: textFromFields(outline),
groups: groups.map((group) => ({
title: textFromFields(group),
items: (Array.isArray(group?.items) ? group.items : []).map((item) => ({
title: textFromFields(item)
})).filter((item) => item.title)
})).filter((group) => group.title || group.items.length)
}
}
export function parseModuleOutlineText(text = '') {
const warnings = []
const lines = String(text ?? '').replace(/\t/g, ' ').split(/\r?\n/)
const contentLines = lines
.map((line, index) => ({ line, number: index + 1, title: line.trim() }))
.filter((entry) => entry.title)
if (!contentLines.length) {
return {
outline: { title: '', groups: [] },
warnings: [warning('EMPTY_OUTLINE')]
}
}
const outline = {
title: contentLines[0].title,
groups: []
}
let currentGroup = null
contentLines.slice(1).forEach((entry) => {
const indent = entry.line.match(/^ */)?.[0].length ?? 0
if (indent < 2) {
currentGroup = createGroup(entry.title)
outline.groups.push(currentGroup)
warnings.push(warning('GROUP_WITHOUT_INDENT', entry.number))
return
}
if (indent < 4) {
currentGroup = createGroup(entry.title)
outline.groups.push(currentGroup)
return
}
if (!currentGroup) {
currentGroup = createGroup(FALLBACK_GROUP_TITLE)
outline.groups.push(currentGroup)
warnings.push(warning('LEAF_WITHOUT_GROUP', entry.number))
}
currentGroup.items.push({ title: entry.title })
})
return {
outline: normalizeModuleOutline(outline),
warnings
}
}
export function blueprintToModuleOutline(blueprint = {}, options = {}) {
const title = cleanText(options.projectName, cleanText(blueprint.projectName, DEFAULT_OUTLINE_TITLE))
const groups = [
{
title: '管理员端',
items: (Array.isArray(blueprint.adminMenus) ? blueprint.adminMenus : []).map((menu, index) => ({
title: menuTitle(menu, index)
})).filter((item) => item.title)
},
{
title: '普通用户端',
items: (Array.isArray(blueprint.frontendMenus) ? blueprint.frontendMenus : []).map((menu, index) => ({
title: menuTitle(menu, index)
})).filter((item) => item.title)
}
].filter((group) => group.items.length)
return normalizeModuleOutline({
title,
groups
})
}
export function moduleOutlineToText(outline = {}) {
const normalized = normalizeModuleOutline(outline)
const lines = []
if (normalized.title) {
lines.push(normalized.title)
}
normalized.groups.forEach((group) => {
lines.push(` ${group.title}`)
group.items.forEach((item) => {
lines.push(` ${item.title}`)
})
})
return lines.join('\n')
}

View File

@@ -0,0 +1,108 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import {
blueprintToModuleOutline,
moduleOutlineToText,
normalizeModuleOutline,
parseModuleOutlineText
} from './appModuleOutline.js'
const studentClubText = `学生社团管理系统
管理员端
注册登录
用户管理
社团管理
成员管理
活动管理
公告管理
系统管理
普通用户端
注册登录
社团浏览
社团申请
活动浏览
活动报名
个人中心`
test('parseModuleOutlineText parses the reference student club outline', () => {
const result = parseModuleOutlineText(studentClubText)
assert.deepEqual(result.warnings, [])
assert.equal(result.outline.title, '学生社团管理系统')
assert.deepEqual(result.outline.groups.map((group) => group.title), ['管理员端', '普通用户端'])
assert.deepEqual(result.outline.groups[0].items.map((item) => item.title), [
'注册登录',
'用户管理',
'社团管理',
'成员管理',
'活动管理',
'公告管理',
'系统管理'
])
assert.deepEqual(result.outline.groups[1].items.map((item) => item.title), [
'注册登录',
'社团浏览',
'社团申请',
'活动浏览',
'活动报名',
'个人中心'
])
})
test('parseModuleOutlineText normalizes tabs as two-space indentation', () => {
const result = parseModuleOutlineText('图书借阅系统\n\t管理端\n\t\t图书管理')
assert.equal(result.outline.title, '图书借阅系统')
assert.equal(result.outline.groups[0].title, '管理端')
assert.equal(result.outline.groups[0].items[0].title, '图书管理')
})
test('parseModuleOutlineText accepts imperfect indentation with warnings', () => {
const result = parseModuleOutlineText('客户管理系统\n客户模块\n 客户列表\n 客户详情')
assert.equal(result.outline.title, '客户管理系统')
assert.equal(result.outline.groups[0].title, '客户模块')
assert.deepEqual(result.outline.groups[0].items.map((item) => item.title), ['客户列表', '客户详情'])
assert.equal(result.warnings.some((warning) => warning.code === 'GROUP_WITHOUT_INDENT'), true)
})
test('parseModuleOutlineText returns empty outline warnings for blank input', () => {
const result = parseModuleOutlineText(' \n\t')
assert.equal(result.outline.title, '')
assert.deepEqual(result.outline.groups, [])
assert.equal(result.warnings.some((warning) => warning.code === 'EMPTY_OUTLINE'), true)
})
test('blueprintToModuleOutline maps project menus into standard groups', () => {
const outline = blueprintToModuleOutline({
projectName: '社团系统',
adminMenus: [
{ code: 'users', name: '用户管理' },
{ code: 'clubs', title: '社团管理' }
],
frontendMenus: [
{ code: 'browse', name: '社团浏览' },
{ code: 'profile' }
]
}, {
projectName: '学生社团管理系统'
})
assert.equal(outline.title, '学生社团管理系统')
assert.deepEqual(outline.groups.map((group) => group.title), ['管理员端', '普通用户端'])
assert.deepEqual(outline.groups[0].items.map((item) => item.title), ['用户管理', '社团管理'])
assert.deepEqual(outline.groups[1].items.map((item) => item.title), ['社团浏览', 'profile'])
})
test('moduleOutlineToText serializes normalized outlines with two-space levels', () => {
const outline = normalizeModuleOutline({
title: '学生社团管理系统',
groups: [
{ title: '管理员端', items: [{ title: '用户管理' }] },
{ title: '普通用户端', items: [{ title: '社团浏览' }] }
]
})
assert.equal(moduleOutlineToText(outline), '学生社团管理系统\n 管理员端\n 用户管理\n 普通用户端\n 社团浏览')
})