diff --git a/plugins/adr-backend/src/search/madrParser.ts b/plugins/adr-backend/src/search/madrParser.ts index c992ef125e..0dd1437538 100644 --- a/plugins/adr-backend/src/search/madrParser.ts +++ b/plugins/adr-backend/src/search/madrParser.ts @@ -16,7 +16,10 @@ import { DateTime } from 'luxon'; import { marked } from 'marked'; -import { MADR_DATE_FORMAT } from '@backstage/plugin-adr-common'; +import { + MADR_DATE_FORMAT, + parseMadrWithFrontmatter, +} from '@backstage/plugin-adr-common'; const getTitle = (tokens: marked.TokensList): string | undefined => { return ( @@ -26,22 +29,6 @@ const getTitle = (tokens: marked.TokensList): string | undefined => { )?.text; }; -const getStatusForV3Format = (tokens: marked.TokensList): string | undefined => - ( - tokens.find( - t => - t.type === 'heading' && - t.depth === 2 && - t.text.toLowerCase().includes('status:'), - ) as marked.Tokens.Text - )?.text - .toLowerCase() - .split('\n') - .find(l => /^status:/.test(l)) - ?.replace(/^status:/i, '') - .trim() - .toLocaleLowerCase('en-US'); - const getStatusForV2Format = (tokens: marked.TokensList): string | undefined => (tokens.find(t => t.type === 'list') as marked.Tokens.List)?.items ?.find(t => /^status:/i.test(t.text)) @@ -49,71 +36,45 @@ const getStatusForV2Format = (tokens: marked.TokensList): string | undefined => .trim() .toLocaleLowerCase('en-US'); -const getDateForV3Format = ( - tokens: marked.TokensList, - dateFormat: string, -): string | undefined => { - let adrDateTime: DateTime | undefined; - const dateLine = ( - tokens.find( - t => - t.type === 'heading' && - t.depth === 2 && - t.text.toLowerCase().includes('date:'), - ) as marked.Tokens.Text - )?.text - .toLowerCase() - .split('\n') - .find(l => /^date:/.test(l)); - - if (dateLine) { - adrDateTime = DateTime.fromFormat( - dateLine.replace(/^date:/i, '').trim(), - dateFormat, - ); - } - return adrDateTime?.isValid - ? adrDateTime.toFormat(MADR_DATE_FORMAT) - : undefined; -}; - -const getDateForV2Format = ( - tokens: marked.TokensList, - dateFormat: string, -): string | undefined => { +const getDateForV2Format = (tokens: marked.TokensList): string | undefined => { const listTokens = (tokens.find(t => t.type === 'list') as marked.Tokens.List) ?.items; - const adrDateTime = DateTime.fromFormat( - listTokens - ?.find(t => /^date:/i.test(t.text)) - ?.text.replace(/^date:/i, '') - .trim() ?? '', - dateFormat, - ); - return adrDateTime?.isValid - ? adrDateTime.toFormat(MADR_DATE_FORMAT) - : undefined; + const adrDateTime = listTokens + ?.find(t => /^date:/i.test(t.text)) + ?.text.replace(/^date:/i, '') + .trim(); + return adrDateTime; }; -const getStatus = (tokens: marked.TokensList): string | undefined => { - return getStatusForV3Format(tokens) ?? getStatusForV2Format(tokens); +const getStatus = ( + tokens: marked.TokensList, + frontMatterStatus?: string, +): string | undefined => { + return frontMatterStatus ?? getStatusForV2Format(tokens); }; const getDate = ( tokens: marked.TokensList, dateFormat: string, -): string | undefined => - getDateForV3Format(tokens, dateFormat) ?? - getDateForV2Format(tokens, dateFormat); + frontMatterDate?: string, +): string | undefined => { + const dateString = frontMatterDate ?? getDateForV2Format(tokens); + if (!dateString) { + return undefined; + } + const date = DateTime.fromFormat(dateString, dateFormat); + return date?.isValid ? date.toFormat(MADR_DATE_FORMAT) : undefined; +}; export const madrParser = (content: string, dateFormat = MADR_DATE_FORMAT) => { - const tokens = marked.lexer(content); + const preparsed = parseMadrWithFrontmatter(content); + const tokens = marked.lexer(preparsed.content); if (!tokens.length) { throw new Error('ADR has no content'); } return { title: getTitle(tokens), - status: getStatus(tokens), - date: getDate(tokens, dateFormat), + status: getStatus(tokens, preparsed.status), + date: getDate(tokens, dateFormat, preparsed.date), }; }; diff --git a/plugins/adr-common/package.json b/plugins/adr-common/package.json index adbb6bea58..63bd30cb97 100644 --- a/plugins/adr-common/package.json +++ b/plugins/adr-common/package.json @@ -31,7 +31,8 @@ "dependencies": { "@backstage/catalog-model": "workspace:^", "@backstage/integration": "workspace:^", - "@backstage/plugin-search-common": "workspace:^" + "@backstage/plugin-search-common": "workspace:^", + "front-matter": "^4.0.2" }, "devDependencies": { "@backstage/cli": "workspace:^" diff --git a/plugins/adr-common/src/index.ts b/plugins/adr-common/src/index.ts index cf71367cba..194f4899d1 100644 --- a/plugins/adr-common/src/index.ts +++ b/plugins/adr-common/src/index.ts @@ -20,6 +20,7 @@ import { Entity, getEntitySourceLocation } from '@backstage/catalog-model'; import { IndexableDocument } from '@backstage/plugin-search-common'; import { ScmIntegrationRegistry } from '@backstage/integration'; +import frontMatter from 'front-matter'; /** * ADR plugin annotation. @@ -101,3 +102,43 @@ export interface AdrDocument extends IndexableDocument { */ date?: string; } + +/** + * Parsed MADR document with front matter (if present) parsed and extracted from the main markdown content. + * @public + */ +export interface ParsedMadr { + /** + * Main body of ADR content (with any front matter removed) + */ + content: string; + /** + * ADR status + */ + status?: string; + /** + * ADR date + */ + date?: string; + /** + * All attributes parsed from front matter + */ + attributes: Record; +} + +/** + * Utility function to parse raw markdown content for an ADR and extract any metadata found as "front matter" at the top of the Markdown document. + * @param content - Raw markdown content which may (optionally) include front matter + * @public + */ +export const parseMadrWithFrontmatter = (content: string): ParsedMadr => { + const parsed = frontMatter>(content); + const status = parsed.attributes.status; + const date = parsed.attributes.date; + return { + content: parsed.body, + status: status ? String(status) : undefined, + date: date ? String(date) : undefined, + attributes: parsed.attributes, + }; +}; diff --git a/plugins/adr/src/components/AdrReader/AdrReader.tsx b/plugins/adr/src/components/AdrReader/AdrReader.tsx index ad99ce3a5a..6b07eee4cd 100644 --- a/plugins/adr/src/components/AdrReader/AdrReader.tsx +++ b/plugins/adr/src/components/AdrReader/AdrReader.tsx @@ -59,6 +59,7 @@ export const AdrReader = (props: { const adrDecorators = decorators ?? [ adrDecoratorFactories.createRewriteRelativeLinksDecorator(), adrDecoratorFactories.createRewriteRelativeEmbedsDecorator(), + adrDecoratorFactories.createFrontMatterFormatterDecorator(), ]; return adrDecorators.reduce( diff --git a/plugins/adr/src/components/AdrReader/decorators.ts b/plugins/adr/src/components/AdrReader/decorators.ts index f24bb48a00..980eab5179 100644 --- a/plugins/adr/src/components/AdrReader/decorators.ts +++ b/plugins/adr/src/components/AdrReader/decorators.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { parseMadrWithFrontmatter } from '@backstage/plugin-adr-common'; import { AdrContentDecorator } from './types'; /** @@ -44,4 +45,23 @@ export const adrDecoratorFactories = Object.freeze({ ), }); }, + /** + * Formats YAML front-matter into a table format (if any exists in the markdown document) + */ + createFrontMatterFormatterDecorator(): AdrContentDecorator { + return ({ content }) => { + const parsedFrontmatter = parseMadrWithFrontmatter(content); + let table = ''; + const attrs = parsedFrontmatter.attributes; + if (Object.keys(attrs).length > 0) { + const stripNewLines = (val: unknown) => + String(val).replaceAll('\n', '
'); + const row = (vals: string[]) => `|${vals.join('|')}|\n`; + table = `${row(Object.keys(attrs))}`; + table += `${row(Object.keys(attrs).map(() => '---'))}`; + table += `${row(Object.values(attrs).map(stripNewLines))}\n\n`; + } + return { content: table + parsedFrontmatter.content }; + }; + }, }); diff --git a/yarn.lock b/yarn.lock index e29e353cc6..d5fe3b1ec1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4521,6 +4521,7 @@ __metadata: "@backstage/cli": "workspace:^" "@backstage/integration": "workspace:^" "@backstage/plugin-search-common": "workspace:^" + front-matter: ^4.0.2 languageName: unknown linkType: soft @@ -25387,6 +25388,15 @@ __metadata: languageName: node linkType: hard +"front-matter@npm:^4.0.2": + version: 4.0.2 + resolution: "front-matter@npm:4.0.2" + dependencies: + js-yaml: ^3.13.1 + checksum: a5b4c36d75a820301ebf31db0f677332d189c4561903ab6853eaa0504b43634f98557dbf87752e09043dbd2c9dcc14b4bcf9151cb319c8ad7e26edb203c0cd23 + languageName: node + linkType: hard + "fs-constants@npm:^1.0.0": version: 1.0.0 resolution: "fs-constants@npm:1.0.0"