Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 133 additions & 0 deletions src/convert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ import { getDescendants } from './resources/questions/common';

type DerivedResourceMap = { [key: string]: TorusResource[] };

type BrokenPageReference = {
pageId: string;
pageTitle: string;
idref: string;
nodeType: 'a' | 'page_link';
};

export function convert(
projectSummary: ProjectSummary,
file: string,
Expand Down Expand Up @@ -314,6 +321,132 @@ export function updateDerivativeReferences(
);
}

const inlineParents = new Set([
'a',
'em',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'li',
'p',
'span',
'strong',
'td',
'th',
]);

function brokenPageLinkText(idref: string) {
return `<broken link: ${idref}>`;
}

function brokenPageLinkNode(idref: string, parentType?: string) {
const text = brokenPageLinkText(idref);

if (parentType && inlineParents.has(parentType)) {
return { text };
}

return {
type: 'p',
children: [{ text }],
};
}

function replaceBrokenPageLinksInNode(
node: any,
validPageIds: Set<string>,
brokenLinks: BrokenPageReference[],
page: Page,
parentType?: string
): any {
if (Array.isArray(node)) {
return node.map((child) =>
replaceBrokenPageLinksInNode(
child,
validPageIds,
brokenLinks,
page,
parentType
)
);
}

if (node === null || typeof node !== 'object') {
return node;
}

if (
(node.type === 'page_link' || node.type === 'a') &&
typeof node.idref === 'string'
) {
if (!validPageIds.has(node.idref)) {
brokenLinks.push({
pageId: page.id,
pageTitle: page.title,
idref: node.idref,
nodeType: node.type,
});

return brokenPageLinkNode(node.idref, parentType);
}

return node;
}

return Object.keys(node).reduce(
(updated: any, key: string) => {
updated[key] = replaceBrokenPageLinksInNode(
node[key],
validPageIds,
brokenLinks,
page,
node.type
);
return updated;
},
{ ...node }
);
}

export function replaceBrokenPageLinks(
resources: TorusResource[]
): TorusResource[] {
const validPageIds = new Set(
resources.filter((r) => r.type === 'Page').map((r) => r.id)
);

const brokenLinks: BrokenPageReference[] = [];

const updated = resources.map((resource) => {
if (resource.type !== 'Page') {
return resource;
}

const page = resource as Page;

return {
...page,
content: replaceBrokenPageLinksInNode(
page.content,
validPageIds,
brokenLinks,
page
),
};
});

brokenLinks.forEach(({ pageId, pageTitle, idref, nodeType }) => {
console.log(
`Warning: Replaced broken ${nodeType} link ${idref} in page ${pageId} (${pageTitle})`
);
});

return updated;
}

export function createProducts(
resources: TorusResource[],
orgPaths: string[],
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,7 @@ export function convertAction(options: CmdOptions): Promise<ConvertedResults> {
let updated = converted;

updated = Convert.updateDerivativeReferences(updated);
updated = Convert.replaceBrokenPageLinks(updated);
updated = Convert.generatePoolTags(updated);
updated = Convert.fixWildcardSelections(updated);
updated = Convert.setSelectionPoints(updated);
Expand Down
81 changes: 80 additions & 1 deletion test/convert-test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as path from 'path';
import { convertAction } from 'src/index';
import { Hierarchy } from 'src/resources/resource';
import { replaceBrokenPageLinks } from 'src/convert';
import { Hierarchy, Page, TorusResource } from 'src/resources/resource';

// migration-4sdfykby_v_1_0-echo is an echo course downloaded from svn.
// Though the .svn repo is not committed to VCS, the intent is that any updates
Expand Down Expand Up @@ -230,3 +231,81 @@ it('should convert content with purpose to groups', async () => {
})
);
});

it('should replace broken page links with visible placeholder text', () => {
const warn = jest.spyOn(console, 'log').mockImplementation(() => undefined);

const resources: TorusResource[] = [
{
type: 'Page',
id: 'page-a',
legacyPath: '',
legacyId: 'page-a',
title: 'Page A',
tags: [],
unresolvedReferences: ['missing-page'],
warnings: [],
content: {
model: [
{
type: 'content',
id: 'content-1',
children: [
{
type: 'page_link',
idref: 'missing-page',
children: [{ text: ' ' }],
},
{
type: 'a',
idref: 'missing-anchor-page',
children: [{ text: 'broken xref' }],
},
{
type: 'a',
href: 'https://example.org',
children: [{ text: 'external link' }],
},
],
},
],
},
isGraded: false,
isSurvey: false,
collabSpace: {
status: 'disabled',
threaded: true,
auto_accept: true,
show_full_history: true,
participation_min_replies: 0,
participation_min_posts: 0,
},
objectives: [],
} as Page,
];

const updated = replaceBrokenPageLinks(resources);
const page = updated[0] as Page;

expect((page.content as any).model[0].children[0]).toEqual({
type: 'p',
children: [{ text: '<broken link: missing-page>' }],
});
expect((page.content as any).model[0].children[1]).toEqual({
type: 'p',
children: [{ text: '<broken link: missing-anchor-page>' }],
});
expect((page.content as any).model[0].children[2]).toEqual({
type: 'a',
href: 'https://example.org',
children: [{ text: 'external link' }],
});
expect(warn).toHaveBeenCalledWith(
'Warning: Replaced broken page_link link missing-page in page page-a (Page A)'
);
expect(warn).toHaveBeenCalledWith(
'Warning: Replaced broken a link missing-anchor-page in page page-a (Page A)'
);

warn.mockRestore();
});
Loading