Skip to content
This repository was archived by the owner on Mar 8, 2019. It is now read-only.

Commit 3350b97

Browse files
committed
feat: extract type and description of functional props
closes #86
1 parent c61afc9 commit 3350b97

File tree

6 files changed

+168
-54
lines changed

6 files changed

+168
-54
lines changed

src/template-handlers/__tests__/propHandler.ts

+14-4
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,19 @@ describe('slotHandler', () => {
1414
[
1515
'<div>',
1616
' <h1>titleof the template</h1>',
17-
' <button :style="`width:${props.size}`"></button>',
17+
' <!-- @prop {number} size width of the button -->',
18+
' <!-- @prop {string} value value in the form -->',
19+
' <button :style="`width:${props.size}`" :value="props.value"></button>',
1820
'</div>',
1921
].join('\n'),
2022
{ comments: true },
2123
).ast
2224
if (ast) {
2325
traverse(ast, doc, [propHandler], { functional: true, rootLeadingComment: '' })
24-
expect(doc.toObject().props).toMatchObject({ size: { type: { name: 'undefined' } } })
26+
expect(doc.toObject().props).toMatchObject({
27+
size: { type: { name: 'number' }, description: 'width of the button' },
28+
value: { type: { name: 'string' }, description: 'value in the form' },
29+
})
2530
} else {
2631
fail()
2732
}
@@ -33,15 +38,20 @@ describe('slotHandler', () => {
3338
'<div>',
3439
' <h1>titleof the template</h1>',
3540
' <button style="width:200px">',
36-
' test {{props.name}}',
41+
' <!-- @prop name Your Name -->',
42+
' <!-- @prop {string} adress Your Adress -->',
43+
' test {{props.name}} {{props.adress}}',
3744
' </button>',
3845
'</div>',
3946
].join('\n'),
4047
{ comments: true },
4148
).ast
4249
if (ast) {
4350
traverse(ast, doc, [propHandler], { functional: true, rootLeadingComment: '' })
44-
expect(doc.toObject().props).toMatchObject({ name: { type: { name: 'undefined' } } })
51+
expect(doc.toObject().props).toMatchObject({
52+
name: { type: { name: 'mixed' }, description: 'Your Name' },
53+
adress: { type: { name: 'string' }, description: 'Your Adress' },
54+
})
4555
} else {
4656
fail()
4757
}

src/template-handlers/propHandler.ts

+47-10
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
import * as bt from '@babel/types'
22
import recast from 'recast'
3-
import { ASTElement, ASTExpression } from 'vue-template-compiler'
3+
import { ASTElement, ASTExpression, ASTNode } from 'vue-template-compiler'
44
import buildParser from '../babel-parser'
5-
import { Documentation } from '../Documentation'
5+
import { Documentation, ParamTag } from '../Documentation'
66
import { TemplateParserOptions } from '../parse-template'
7+
import extractLeadingComment from '../utils/extractLeadingComment'
8+
import getDoclets from '../utils/getDoclets'
9+
10+
const parser = buildParser({ plugins: ['typescript'] })
711

812
const allowRE = /^(v-bind|:)/
913
export default function propTemplateHandler(
@@ -12,35 +16,50 @@ export default function propTemplateHandler(
1216
options: TemplateParserOptions,
1317
) {
1418
if (options.functional) {
15-
propsInAttributes(templateAst, documentation)
16-
propsInInterpolation(templateAst, documentation)
19+
propsInAttributes(templateAst, documentation, options)
20+
propsInInterpolation(templateAst, documentation, options)
1721
}
1822
}
1923

20-
function propsInAttributes(templateAst: ASTElement, documentation: Documentation) {
24+
function propsInAttributes(
25+
templateAst: ASTElement,
26+
documentation: Documentation,
27+
options: TemplateParserOptions,
28+
) {
2129
const bindings = templateAst.attrsMap
2230
const keys = Object.keys(bindings)
2331
for (const key of keys) {
2432
// only look at expressions
2533
if (allowRE.test(key)) {
2634
const expression = bindings[key]
27-
getPropsFromExpression(expression, documentation)
35+
getPropsFromExpression(templateAst.parent, templateAst, expression, documentation, options)
2836
}
2937
}
3038
}
3139

32-
function propsInInterpolation(templateAst: ASTElement, documentation: Documentation) {
40+
function propsInInterpolation(
41+
templateAst: ASTElement,
42+
documentation: Documentation,
43+
options: TemplateParserOptions,
44+
) {
3345
if (templateAst.children) {
3446
templateAst.children
3547
.filter(c => c.type === 2)
3648
.forEach((expr: ASTExpression) => {
37-
getPropsFromExpression(expr.expression, documentation)
49+
getPropsFromExpression(templateAst, expr, expr.expression, documentation, options)
3850
})
3951
}
4052
}
4153

42-
function getPropsFromExpression(expression: string, documentation: Documentation) {
43-
const ast = buildParser({ plugins: ['typescript'] }).parse(expression)
54+
function getPropsFromExpression(
55+
parentAst: ASTElement | undefined,
56+
item: ASTNode,
57+
expression: string,
58+
documentation: Documentation,
59+
options: TemplateParserOptions,
60+
) {
61+
const ast = parser.parse(expression)
62+
const propsFound: string[] = []
4463
recast.visit(ast.program, {
4564
visitMemberExpression(path) {
4665
const obj = path.node ? path.node.object : undefined
@@ -54,9 +73,27 @@ function getPropsFromExpression(expression: string, documentation: Documentation
5473
) {
5574
const pName = propName.name
5675
const p = documentation.getPropDescriptor(pName)
76+
propsFound.push(pName)
5777
p.type = { name: 'undefined' }
5878
}
5979
return false
6080
},
6181
})
82+
if (propsFound.length) {
83+
const comment = extractLeadingComment(parentAst, item, options.rootLeadingComment)
84+
const doclets = getDoclets(comment)
85+
const propTags = doclets.tags && (doclets.tags.filter(d => d.title === 'prop') as ParamTag[])
86+
if (propTags && propTags.length) {
87+
propsFound.forEach(pName => {
88+
const propTag = propTags.filter(pt => pt.name === pName)
89+
if (propTag.length) {
90+
const p = documentation.getPropDescriptor(pName)
91+
p.type = propTag[0].type
92+
if (typeof propTag[0].description === 'string') {
93+
p.description = propTag[0].description
94+
}
95+
}
96+
})
97+
}
98+
}
6299
}

src/template-handlers/slotHandler.ts

+7-39
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { ASTElement, ASTNode } from 'vue-template-compiler'
1+
import { ASTElement } from 'vue-template-compiler'
22
import { Documentation } from '../Documentation'
33
import { TemplateParserOptions } from '../parse-template'
4+
import extractLeadingComment from '../utils/extractLeadingComment'
45

56
export default function slotHandler(
67
documentation: Documentation,
@@ -22,45 +23,12 @@ export default function slotHandler(
2223
}
2324

2425
slotDescriptor.bindings = bindings
25-
let comment = ''
26-
if (templateAst.parent) {
27-
const slotSiblings: ASTNode[] = templateAst.parent.children
28-
// First find the position of the slot in the list
29-
let i = slotSiblings.length - 1
30-
let currentSlotIndex = -1
31-
do {
32-
if (slotSiblings[i] === templateAst) {
33-
currentSlotIndex = i
34-
}
35-
} while (currentSlotIndex < 0 && i--)
36-
37-
// Find the first leading comment node as a description of the slot
38-
const slotSiblingsBeforeSlot = slotSiblings.slice(0, currentSlotIndex).reverse()
39-
40-
for (const potentialComment of slotSiblingsBeforeSlot) {
41-
// if there is text between the slot and the comment, ignore
42-
if (
43-
potentialComment.type !== 3 ||
44-
(!potentialComment.isComment && potentialComment.text.trim())
45-
) {
46-
break
47-
}
48-
49-
if (
50-
potentialComment.isComment &&
51-
!(
52-
templateAst.parent.tag === 'slot' && templateAst.parent.children[0] === potentialComment
53-
)
54-
) {
55-
comment = potentialComment.text.trim()
56-
57-
break
58-
}
59-
}
60-
} else if (options.rootLeadingComment.length) {
61-
comment = options.rootLeadingComment
62-
}
6326

27+
const comment = extractLeadingComment(
28+
templateAst.parent,
29+
templateAst,
30+
options.rootLeadingComment,
31+
)
6432
if (comment.length && comment.search(/\@slot/) !== -1) {
6533
slotDescriptor.description = comment.replace('@slot', '').trim()
6634
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import extractLeadingComment from '../extractLeadingComment'
2+
import { ASTElement, compile } from 'vue-template-compiler'
3+
4+
function compileIt(src: string): ASTElement | undefined {
5+
const ast = compile(src, { comments: true }).ast
6+
if (ast) {
7+
const firstHeader = ast.children.filter((a: ASTElement) => a.tag === 'h1') as ASTElement[]
8+
if (firstHeader.length) {
9+
return firstHeader[0]
10+
}
11+
}
12+
return ast
13+
}
14+
15+
describe('extractLeadingComment', () => {
16+
it('should extract single line comments', () => {
17+
const elt = compileIt(
18+
[
19+
'<div>',
20+
' <div>Hello World !!</div>',
21+
' <div>Happy Day !!</div>',
22+
' <!-- single line comment -->',
23+
' <h1>title of the template</h1>',
24+
'</div>',
25+
].join('\n'),
26+
)
27+
if (!elt) {
28+
fail()
29+
} else {
30+
expect(extractLeadingComment(elt.parent, elt, '')).toBe('single line comment')
31+
}
32+
})
33+
34+
it('should extract multi line comments', () => {
35+
const elt = compileIt(
36+
[
37+
'<div>',
38+
' <div>Hello World !!</div>',
39+
' <!-- multi line comment -->',
40+
' <!-- on 2 lines -->',
41+
' <h1>title of the template</h1>',
42+
'</div>',
43+
].join('\n'),
44+
)
45+
if (elt) {
46+
expect(extractLeadingComment(elt.parent, elt, '')).toBe(
47+
['multi line comment', 'on 2 lines'].join('\n'),
48+
)
49+
} else {
50+
fail()
51+
}
52+
})
53+
})

src/utils/extractLeadingComment.ts

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { ASTElement, ASTNode } from 'vue-template-compiler'
2+
3+
/**
4+
* Extract leading comments to an html node
5+
* Even if the comment is on multiple lines it's still taken as a whole
6+
* @param templateAst
7+
* @param rootLeadingComment
8+
*/
9+
export default function extractLeadingComment(
10+
parentAst: ASTElement | undefined,
11+
templateAst: ASTNode,
12+
rootLeadingComment: string,
13+
): string {
14+
let comment = ''
15+
if (parentAst) {
16+
const slotSiblings: ASTNode[] = parentAst.children
17+
// First find the position of the slot in the list
18+
let i = slotSiblings.length - 1
19+
let currentSlotIndex = -1
20+
do {
21+
if (slotSiblings[i] === templateAst) {
22+
currentSlotIndex = i
23+
}
24+
} while (currentSlotIndex < 0 && i--)
25+
26+
// Find the first leading comment
27+
const slotSiblingsBeforeSlot = slotSiblings.slice(0, currentSlotIndex).reverse()
28+
29+
for (const potentialComment of slotSiblingsBeforeSlot) {
30+
// if there is text between the slot and the comment, ignore
31+
if (
32+
potentialComment.type !== 3 ||
33+
(!potentialComment.isComment && potentialComment.text.trim().length)
34+
) {
35+
break
36+
}
37+
38+
if (potentialComment.isComment) {
39+
comment = potentialComment.text.trim() + '\n' + comment
40+
}
41+
}
42+
} else if (rootLeadingComment.length) {
43+
comment = rootLeadingComment
44+
}
45+
return comment.trim()
46+
}

src/utils/getDoclets.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ function getTypeObjectFromTypeString(typeSlice: string): ParamType {
4545
}
4646
}
4747

48-
const TYPED_TAG_TITLES = ['param', 'property', 'type', 'returns']
48+
const TYPED_TAG_TITLES = ['param', 'property', 'type', 'returns', 'prop']
4949
const ACCESS_TAG_TITLES = ['private', 'public']
5050

5151
/**

0 commit comments

Comments
 (0)