build(docs-infra): support doc aliases via `@alias` dgeni tag (#29673)

Now, one can add an `@alias` tag to API docs, which tells dgeni that this
API element (usually a `const`) is really just an alias for some API element
defined elsewhere.

Dgeni will then look up this API element and copy over the properties from
the alias to the current doc.

For example, we would like to privately export an Enum from `@angular/core`
but then publicly export this from `@angular/common`:

**packages/core/private_exports.ts**

```ts
/**
 * Description of this document.
 */
export enum ɵSomeEnum { ... }
```

**packages/common/public_api.ts**

```ts
import {ɵSomeEnum} from '@angular/core';

 /**
 * @alias core/ɵSomeEnum
 */
export const SomeEnum = ɵSomeEnum;
```

In the generated docs there will be a page for `common/SomeEnum`, which
will be rendered as an enum, rather than a const, showing the description
extracted from the `core/ɵSomeEnum`.

---

The implementation of this feature required some refactoring of the other
processing:

1. Previously `ɵ` prefixed exports were not even considered.
2. Due to 1. some processors needed to have guards added to ignore such
   private exports (`addMetadataAliases` and `checkContentRules`).
3. The processing of package pages had to be reworked (and split) so that
   it picked up the aliased export docs after their alias proeprties had
   been copied.

See FW-1207, FW-632, #29249

PR Close #29673
This commit is contained in:
Pete Bacon Darwin 2019-04-02 22:24:29 +01:00 committed by Jason Aden
parent 6233cd55f7
commit a4f3f3f81d
13 changed files with 297 additions and 93 deletions

View File

@ -32,9 +32,11 @@ module.exports = new Package('angular-api', [basePackage, typeScriptPackage])
.processor(require('./processors/simplifyMemberAnchors'))
.processor(require('./processors/computeStability'))
.processor(require('./processors/removeInjectableConstructors'))
.processor(require('./processors/collectPackageContentDocs'))
.processor(require('./processors/processPackages'))
.processor(require('./processors/processNgModuleDocs'))
.processor(require('./processors/fixupRealProjectRelativePath'))
.processor(require('./processors/processAliasDocs'))
/**
@ -72,7 +74,7 @@ module.exports = new Package('angular-api', [basePackage, typeScriptPackage])
// API files are typescript
readTypeScriptModules.basePath = API_SOURCE_PATH;
readTypeScriptModules.ignoreExportsMatching = [/^[_ɵ]|^VERSION$/];
readTypeScriptModules.ignoreExportsMatching = [/^[_]|^VERSION$/];
readTypeScriptModules.hidePrivateMembers = true;
// NOTE: This list should be in sync with tools/public_api_guard/BUILD.bazel

View File

@ -15,9 +15,13 @@ module.exports = function addMetadataAliasesProcessor() {
docs.forEach(doc => {
switch(doc.docType) {
case 'directive':
case 'component':
doc.aliases = doc.aliases.concat(extractSelectors(doc[doc.docType + 'Options'].selector));
case 'component': {
const selector = doc[doc.docType + 'Options'].selector;
if (selector) {
doc.aliases = doc.aliases.concat(extractSelectors(selector));
}
break;
}
case 'pipe':
if (doc.pipeOptions.name) {
doc.aliases = doc.aliases.concat(stripQuotes(doc.pipeOptions.name));

View File

@ -54,4 +54,15 @@ describe('addSelectorsAsAliases processor', () => {
expect(docs[11].aliases).toEqual([docs[11].name]);
expect(docs[12].aliases).toEqual([docs[12].name]);
});
it('should ignore directives and components that have no selector', () => {
const processor = processorFactory();
const docs = [
{ docType: 'directive', name: 'MyDirective', aliases: ['MyDirective'], directiveOptions: { } },
{ docType: 'component', name: 'MyComponent', aliases: ['MyComponent'], componentOptions: { } },
];
processor.$process(docs);
expect(docs[0].aliases).toEqual([docs[0].name]);
expect(docs[1].aliases).toEqual([docs[1].name]);
});
});

View File

@ -0,0 +1,19 @@
const { dirname } = require('canonical-path');
module.exports = function collectPackageContentDocsProcessor() {
return {
$runAfter: ['tags-extracted'],
$runBefore: ['computing-ids', 'processPackages'],
packageContentFiles: {},
$process(docs) {
return docs.filter(doc => {
if (doc.docType === 'package-content') {
this.packageContentFiles[dirname(doc.fileInfo.filePath)] = doc;
return false;
} else {
return true;
}
});
}
};
};

View File

@ -0,0 +1,49 @@
const testPackage = require('../../helpers/test-package');
const processorFactory = require('./collectPackageContentDocs');
const Dgeni = require('dgeni');
describe('collectPackageContentDocs processor', () => {
it('should be available on the injector', () => {
const dgeni = new Dgeni([testPackage('angular-api-package')]);
const injector = dgeni.configureInjector();
const processor = injector.get('collectPackageContentDocsProcessor');
expect(processor.$process).toBeDefined();
expect(processor.$runAfter).toEqual(['tags-extracted']);
expect(processor.$runBefore).toEqual(['computing-ids', 'processPackages']);
});
it('should collect any `package-content` docs in the `packageContentFiles` map', () => {
const docs = [
{ fileInfo: { filePath: 'some/a' }, docType: 'a', id: 'a' },
{ fileInfo: { filePath: 'some/x/PACKAGE.md' }, docType: 'package-content', id: 'x' },
{ fileInfo: { filePath: 'some/b' }, docType: 'b', id: 'b' },
{ fileInfo: { filePath: 'some/y/PACKAGE.md' }, docType: 'package-content', id: 'y' },
{ fileInfo: { filePath: 'some/z/PACKAGE.md' }, docType: 'package-content', id: 'z' },
];
const processor = processorFactory();
processor.$process(docs);
expect(processor.packageContentFiles).toEqual({
'some/x': { fileInfo: { filePath: 'some/x/PACKAGE.md' }, docType: 'package-content', id: 'x' },
'some/y': { fileInfo: { filePath: 'some/y/PACKAGE.md' }, docType: 'package-content', id: 'y' },
'some/z': { fileInfo: { filePath: 'some/z/PACKAGE.md' }, docType: 'package-content', id: 'z' },
});
});
it('should filter out any `package-content` docs from the collection', () => {
const docs = [
{ fileInfo: { filePath: 'some/a' }, docType: 'a', id: 'a' },
{ fileInfo: { filePath: 'some/x/PACKAGE.md' }, docType: 'package-content', id: 'x' },
{ fileInfo: { filePath: 'some/b' }, docType: 'b', id: 'b' },
{ fileInfo: { filePath: 'some/y/PACKAGE.md' }, docType: 'package-content', id: 'y' },
{ fileInfo: { filePath: 'some/z/PACKAGE.md' }, docType: 'package-content', id: 'z' },
];
const processor = processorFactory();
const newDocs = processor.$process(docs);
expect(newDocs).toEqual([
{ fileInfo: { filePath: 'some/a' }, docType: 'a', id: 'a' },
{ fileInfo: { filePath: 'some/b' }, docType: 'b', id: 'b' },
]);
});
});

View File

@ -0,0 +1,40 @@
/**
* Copies over the properties from a doc's alias if it is marked with `@alias`.
*/
module.exports = function processAliasDocs(getDocFromAlias, log, createDocMessage) {
return {
$runAfter: ['tags-extracted', 'ids-computed'],
$runBefore: ['filterPrivateDocs'],
propertiesToKeep: [
'name', 'id', 'aliases', 'fileInfo', 'startingLine', 'endingLine',
'path', 'originalModule', 'outputPath', 'privateExport', 'moduleDoc'
],
$process(docs) {
docs.forEach(doc => {
if (doc.aliasDocId) {
const aliasDocs = getDocFromAlias(doc.aliasDocId, doc);
if (aliasDocs.length === 1) {
const aliasDoc = aliasDocs[0];
log.debug('processing alias', doc.id, doc.aliasDocId, aliasDoc.id);
// Clean out the unwanted properties from the doc
Object.keys(doc).forEach(key => {
if (!this.propertiesToKeep.includes(key)) {
delete doc[key];
}
});
// Copy over all the properties of the alias doc.
Object.keys(aliasDoc).forEach(key => {
if (!this.propertiesToKeep.includes(key)) {
doc[key] = aliasDoc[key];
}
});
} else if (aliasDocs.length === 0) {
throw new Error(createDocMessage(`There is no doc that matches "@alias ${doc.aliasDocId}"`, doc));
} else {
throw new Error(createDocMessage(`There is more than one doc that matches "@alias ${doc.aliasDocId}": ${aliasDocs.map(d => d.id).join(', ')}.`, doc));
}
}
});
}
};
};

View File

@ -0,0 +1,83 @@
const testPackage = require('../../helpers/test-package');
const processorFactory = require('./processAliasDocs');
const Dgeni = require('dgeni');
const mockLogFactory = require('dgeni/lib/mocks/log');
const createDocMessageFactory = require('dgeni-packages/base/services/createDocMessage');
describe('processAliasDocs processor', () => {
let getDocFromAlias, log, createDocMessage, processor;
beforeEach(() => {
getDocFromAlias = jasmine.createSpy('getDocFromAlias');
log = mockLogFactory(false);
createDocMessage = createDocMessageFactory();
processor = processorFactory(getDocFromAlias, log, createDocMessage);
});
it('should be available on the injector', () => {
const dgeni = new Dgeni([testPackage('angular-api-package')]);
const injector = dgeni.configureInjector();
const processor = injector.get('processAliasDocs');
expect(processor.$process).toBeDefined();
});
it('should run before the correct processor', () => {
expect(processor.$runBefore).toEqual(['filterPrivateDocs']);
});
it('should run after the correct processor', () => {
expect(processor.$runAfter).toEqual(['tags-extracted', 'ids-computed']);
});
it('should ignore docs that do not have an `@alias` tag', () => {
const docs = [{}];
getDocFromAlias.and.returnValue([{ prop1: 'prop-1', prop2: 'prop-2', prop3: 'prop-3' }]);
processor.$process(docs);
expect(docs).toEqual([{}]);
});
it('should copy over properties from a valid alias doc', () => {
const docs = [{ aliasDocId: 'alias-doc' }];
getDocFromAlias.and.returnValue([{ prop1: 'prop-1', prop2: 'prop-2', prop3: 'prop-3' }]);
processor.$process(docs);
expect(docs).toEqual([
{ prop1: 'prop-1', prop2: 'prop-2', prop3: 'prop-3' }
]);
});
it('should error if `@alias` does not match a doc', () => {
const docs = [{ aliasDocId: 'alias-doc' }];
getDocFromAlias.and.returnValue([]);
expect(() => processor.$process(docs)).toThrowError('There is no doc that matches "@alias alias-doc" - doc');
});
it('should error if `@alias` matches more than one doc', () => {
const docs = [{ aliasDocId: 'alias-doc' }];
getDocFromAlias.and.returnValue([{id: 'alias-1'}, {id: 'alias-2'}]);
expect(() => processor.$process(docs)).toThrowError('There is more than one doc that matches "@alias alias-doc": alias-1, alias-2. - doc');
});
it('should remove all but the specified properties from the original doc', () => {
processor.propertiesToKeep = ['x', 'y'];
const docs = [{ aliasDocId: 'alias-doc', x: 'original-x', z: 'original-z' }];
getDocFromAlias.and.returnValue([{}]);
processor.$process(docs);
expect(docs).toEqual([{ x: 'original-x' }]);
});
it('should copy over all but the specified properties from the aliased doc', () => {
processor.propertiesToKeep = ['x', 'y'];
const docs = [{ aliasDocId: 'alias-doc', x: 'original-x', z: 'original-z' }];
getDocFromAlias.and.returnValue([{ x: 'alias-x', y: 'alias-y', z: 'alias-z' }]);
processor.$process(docs);
expect(docs).toEqual([{ x: 'original-x', z: 'alias-z' }]);
});
it('should have default properties to keep', () => {
expect(processor.propertiesToKeep).toEqual([
'name', 'id', 'aliases', 'fileInfo', 'startingLine', 'endingLine',
'path', 'originalModule', 'outputPath', 'privateExport', 'moduleDoc'
]);
});
});

View File

@ -1,22 +1,13 @@
const { dirname } = require('canonical-path');
module.exports = function processPackages() {
module.exports = function processPackages(collectPackageContentDocsProcessor) {
return {
$runAfter: ['extractDecoratedClassesProcessor', 'computeStability'],
$runBefore: ['computing-ids', 'generateKeywordsProcessor'],
$runAfter: ['processAliasDocs', 'collectPackageContentDocsProcessor'],
$runBefore: ['rendering-docs', 'checkContentRules'],
$process(docs) {
const packageContentFiles = {};
const packageContentFiles = collectPackageContentDocsProcessor.packageContentFiles;
const packageMap = {};
docs = docs.filter(doc => {
if (doc.docType === 'package-content') {
packageContentFiles[dirname(doc.fileInfo.filePath)] = doc;
return false;
} else {
return true;
}
});
docs.forEach(doc => {
if (doc.docType === 'module') {
// Convert the doc type from "module" to "package"
@ -26,15 +17,16 @@ module.exports = function processPackages() {
// Partition the exports into groups by type
if (doc.exports) {
doc.ngmodules = doc.exports.filter(doc => doc.docType === 'ngmodule').sort(byId);
doc.classes = doc.exports.filter(doc => doc.docType === 'class').sort(byId);
doc.decorators = doc.exports.filter(doc => doc.docType === 'decorator').sort(byId);
doc.functions = doc.exports.filter(doc => doc.docType === 'function').sort(byId);
doc.structures = doc.exports.filter(doc => doc.docType === 'enum' || doc.docType === 'interface').sort(byId);
doc.directives = doc.exports.filter(doc => doc.docType === 'directive').sort(byId);
doc.pipes = doc.exports.filter(doc => doc.docType === 'pipe').sort(byId);
doc.types = doc.exports.filter(doc => doc.docType === 'type-alias' || doc.docType === 'const').sort(byId);
if (doc.exports.every(doc => !!doc.deprecated)) {
const publicExports = doc.exports.filter(doc => !doc.privateExport);
doc.ngmodules = publicExports.filter(doc => doc.docType === 'ngmodule').sort(byId);
doc.classes = publicExports.filter(doc => doc.docType === 'class').sort(byId);
doc.decorators = publicExports.filter(doc => doc.docType === 'decorator').sort(byId);
doc.functions = publicExports.filter(doc => doc.docType === 'function').sort(byId);
doc.structures = publicExports.filter(doc => doc.docType === 'enum' || doc.docType === 'interface').sort(byId);
doc.directives = publicExports.filter(doc => doc.docType === 'directive').sort(byId);
doc.pipes = publicExports.filter(doc => doc.docType === 'pipe').sort(byId);
doc.types = publicExports.filter(doc => doc.docType === 'type-alias' || doc.docType === 'const').sort(byId);
if (publicExports.every(doc => !!doc.deprecated)) {
doc.deprecated = 'all exports of this entry point are deprecated.';
}
}
@ -66,8 +58,6 @@ module.exports = function processPackages() {
const pkg = packageMap[key];
pkg.primary.packageDeprecated = pkg.primary.deprecated !== undefined && pkg.secondary.every(entryPoint => entryPoint.deprecated !== undefined);
});
return docs;
}
};
};

View File

@ -9,36 +9,19 @@ describe('processPackages processor', () => {
const injector = dgeni.configureInjector();
const processor = injector.get('processPackages');
expect(processor.$process).toBeDefined();
expect(processor.$runAfter).toEqual(['extractDecoratedClassesProcessor', 'computeStability']);
expect(processor.$runBefore).toEqual(['computing-ids', 'generateKeywordsProcessor']);
expect(processor.$runAfter).toEqual(['processAliasDocs', 'collectPackageContentDocsProcessor']);
expect(processor.$runBefore).toEqual(['rendering-docs', 'checkContentRules']);
});
it('should filter out any `package-content` docs from the collection', () => {
const docs = [
{ fileInfo: { filePath: 'some/a' }, docType: 'a', id: 'a' },
{ fileInfo: { filePath: 'some/x' }, docType: 'package-content', id: 'x' },
{ fileInfo: { filePath: 'some/b' }, docType: 'b', id: 'b' },
{ fileInfo: { filePath: 'some/y' }, docType: 'package-content', id: 'y' },
{ fileInfo: { filePath: 'some/z' }, docType: 'package-content', id: 'z' },
];
const processor = processorFactory();
const newDocs = processor.$process(docs);
expect(newDocs).toEqual([
{ fileInfo: { filePath: 'some/a' }, docType: 'a', id: 'a' },
{ fileInfo: { filePath: 'some/b' }, docType: 'b', id: 'b' },
]);
});
it('should change `module` docs to `package` docs', () => {
const processor = processorFactory();
const processor = processorFactory({ packageContentFiles: {} });
const docs = [
{ fileInfo: { filePath: 'some/a' }, docType: 'module', id: 'a' },
{ fileInfo: { filePath: 'some/b' }, docType: 'module', id: 'b' },
{ docType: 'other', id: 'c' },
];
const newDocs = processor.$process(docs);
expect(newDocs).toEqual([
processor.$process(docs);
expect(docs).toEqual([
jasmine.objectContaining({ docType: 'package', id: 'a' }),
jasmine.objectContaining({ docType: 'package', id: 'b' }),
jasmine.objectContaining({ docType: 'other', id: 'c' }),
@ -54,21 +37,23 @@ describe('processPackages processor', () => {
someProp: 'foo',
},
{
fileInfo: { filePath: 'some/package-2/index' },
docType: 'module',
id: 'package-2',
},
];
const packageContentFiles = {
'some/package-1': {
fileInfo: { filePath: 'some/package-1/PACKAGE.md' },
docType: 'package-content',
id: 'package-1/PACKAGE.md',
shortDescription: 'some short description',
description: 'some description',
see: [ 'a', 'b' ],
},
{
fileInfo: { filePath: 'some/package-2/index' },
docType: 'module',
id: 'package-2',
},
];
const processor = processorFactory();
const newDocs = processor.$process(docs);
}
};
const processor = processorFactory({ packageContentFiles });
processor.$process(docs);
const package1 = jasmine.objectContaining({
fileInfo: { filePath: 'some/package-1/PACKAGE.md' },
@ -90,7 +75,7 @@ describe('processPackages processor', () => {
isPrimaryPackage: true,
});
expect(newDocs).toEqual([package1, package2]);
expect(docs).toEqual([package1, package2]);
});
it('should compute primary and second package info', () => {
@ -111,20 +96,20 @@ describe('processPackages processor', () => {
id: 'package-1/sub-2',
},
];
const processor = processorFactory();
const newDocs = processor.$process(docs);
const processor = processorFactory({ packageContentFiles: {} });
processor.$process(docs);
expect(newDocs[0].isPrimaryPackage).toBe(true);
expect(newDocs[1].isPrimaryPackage).toBe(false);
expect(newDocs[2].isPrimaryPackage).toBe(false);
expect(docs[0].isPrimaryPackage).toBe(true);
expect(docs[1].isPrimaryPackage).toBe(false);
expect(docs[2].isPrimaryPackage).toBe(false);
expect(newDocs[0].packageInfo.primary).toBe(newDocs[0]);
expect(newDocs[1].packageInfo.primary).toBe(newDocs[0]);
expect(newDocs[2].packageInfo.primary).toBe(newDocs[0]);
expect(docs[0].packageInfo.primary).toBe(docs[0]);
expect(docs[1].packageInfo.primary).toBe(docs[0]);
expect(docs[2].packageInfo.primary).toBe(docs[0]);
expect(newDocs[0].packageInfo.secondary).toEqual([newDocs[1], newDocs[2]]);
expect(newDocs[1].packageInfo.secondary).toEqual([newDocs[1], newDocs[2]]);
expect(newDocs[2].packageInfo.secondary).toEqual([newDocs[1], newDocs[2]]);
expect(docs[0].packageInfo.secondary).toEqual([docs[1], docs[2]]);
expect(docs[1].packageInfo.secondary).toEqual([docs[1], docs[2]]);
expect(docs[2].packageInfo.secondary).toEqual([docs[1], docs[2]]);
});
it('should partition the exports of packages into groups, sorted by id', () => {
@ -150,28 +135,28 @@ describe('processPackages processor', () => {
]
},
];
const processor = processorFactory();
const newDocs = processor.$process(docs);
const processor = processorFactory({ packageContentFiles: {} });
processor.$process(docs);
expect(newDocs[0].decorators).toEqual([
expect(docs[0].decorators).toEqual([
{ docType: 'decorator', id: 'decorator-1' },
]);
expect(newDocs[0].functions).toEqual([
expect(docs[0].functions).toEqual([
{ docType: 'function', id: 'function-1' },
]);
expect(newDocs[0].structures).toEqual([
expect(docs[0].structures).toEqual([
{ docType: 'enum', id: 'enum-1' },
{ docType: 'interface', id: 'interface-1' },
{ docType: 'interface', id: 'interface-2' },
]);
expect(newDocs[0].directives).toEqual([
expect(docs[0].directives).toEqual([
{ docType: 'directive', id: 'directive-1' },
{ docType: 'directive', id: 'directive-2' },
]);
expect(newDocs[0].pipes).toEqual([
expect(docs[0].pipes).toEqual([
{ docType: 'pipe', id: 'pipe-1' },
]);
expect(newDocs[0].types).toEqual([
expect(docs[0].types).toEqual([
{ docType: 'const', id: 'const-1' },
{ docType: 'const', id: 'const-2' },
{ docType: 'type-alias', id: 'type-alias-1' },
@ -215,13 +200,13 @@ describe('processPackages processor', () => {
]
},
];
const processor = processorFactory();
const newDocs = processor.$process(docs);
const processor = processorFactory({ packageContentFiles: {} });
processor.$process(docs);
expect(newDocs[0].deprecated).toBeTruthy();
expect(newDocs[1].deprecated).toBeTruthy();
expect(newDocs[2].deprecated).toBeUndefined();
expect(newDocs[3].deprecated).toBeUndefined();
expect(docs[0].deprecated).toBeTruthy();
expect(docs[1].deprecated).toBeTruthy();
expect(docs[2].deprecated).toBeUndefined();
expect(docs[3].deprecated).toBeUndefined();
});
it('should compute the deprecated status of packages', () => {
@ -275,12 +260,12 @@ describe('processPackages processor', () => {
]
},
];
const processor = processorFactory();
const newDocs = processor.$process(docs);
expect(newDocs[0].packageDeprecated).toBe(true);
expect(newDocs[1].packageDeprecated).toBeUndefined();
expect(newDocs[2].packageDeprecated).toBe(false);
expect(newDocs[3].packageDeprecated).toBeUndefined();
expect(newDocs[4].packageDeprecated).toBe(false);
const processor = processorFactory({ packageContentFiles: {} });
processor.$process(docs);
expect(docs[0].packageDeprecated).toBe(true);
expect(docs[1].packageDeprecated).toBeUndefined();
expect(docs[2].packageDeprecated).toBe(false);
expect(docs[3].packageDeprecated).toBeUndefined();
expect(docs[4].packageDeprecated).toBe(false);
});
});

View File

@ -0,0 +1,6 @@
module.exports = function() {
return {
name: 'alias',
docProperty: 'aliasDocId'
};
};

View File

@ -37,6 +37,8 @@ module.exports = function checkContentRules(log, createDocMessage) {
const logMessage = this.failOnContentErrors ? log.error.bind(log) : log.warn.bind(log);
const errors = [];
docs.forEach(doc => {
// Ignore private exports (and members of a private export).
if (doc.id && doc.id.indexOf('ɵ') !== -1) return;
const docErrors = [];
const rules = this.docTypeRules[doc.docType] || {};
if (rules) {

View File

@ -50,7 +50,7 @@ describe('checkContentRules processor', function() {
const docs = [
{ docType: 'test1', description: 'test doc 1', name: 'test-1' },
{ docType: 'test2', description: 'test doc 2', name: 'test-2' }
{ docType: 'test2', description: 'test doc 2', name: 'test-2' },
];
processor.$process(docs);
expect(nameSpy1).toHaveBeenCalledTimes(1);
@ -119,4 +119,17 @@ describe('checkContentRules processor', function() {
- doc "test-1" (test1) `);
});
it('should ignore docs whose id contains a barred-o', () => {
const nameSpy1 = jasmine.createSpy('name 1');
processor.docTypeRules = { 'doc-type': { name: [nameSpy1] } };
const docs = [
{ docType: 'doc-type', id: 'package/class/property/param', name: 'name-1' },
{ docType: 'doc-type', id: 'package/class/property/ɵparam', name: 'name-2' },
{ docType: 'doc-type', id: 'package/class/ɵproperty/param', name: 'name-3' },
{ docType: 'doc-type', id: 'package/ɵclass/property/param', name: 'name-4' },
];
processor.$process(docs);
expect(nameSpy1).toHaveBeenCalledTimes(1);
expect(nameSpy1).toHaveBeenCalledWith(docs[0], 'name', 'name-1');
});
});

View File

@ -12,7 +12,7 @@
{% endmacro -%}
{%- macro renderDescendants(doc, descendantType, title='', recursed=true, docTypeMatcher=descendantType) %}
{% set descendants = doc.descendants | filterByPropertyValue('docType', docTypeMatcher) %}
{% set descendants = doc.descendants | filterByPropertyValue('docType', docTypeMatcher) | filterByPropertyValue('privateExport', undefined) %}
{% if descendants.length %}
<div class="descendants {$ descendantType $}">
{% if title %}<h2>{$ title $}</h2>{% endif %}