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
4 changes: 3 additions & 1 deletion src/media.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ const TARGET_SPECS: TargetSpec[] = [
{ selector: 'embed_activity source', attr: 'text' },
{ selector: 'linked_activity source', attr: 'text' },
{ selector: 'asset, interface, dataset', attr: 'text' },
{ selector: 'applet param wb\\:path', attr: 'href' },
// in case process HTML, not used in OLI xml
{ selector: 'script', attr: 'src' },
{ selector: 'img', attr: 'src' },
Expand Down Expand Up @@ -178,7 +179,8 @@ export function transformToFlatDirectory(
$(e).parent()[0].name === 'linked_activity'));

const isWebBundleElement = (e: cheerio.Element) =>
['link', 'iframe'].includes($(e)[0].name) || isSuperactivityAsset(e);
['link', 'iframe', 'wb:path'].includes($(e)[0].name) ||
isSuperactivityAsset(e);

const targets = collectTargets($, 'relative');

Expand Down
85 changes: 78 additions & 7 deletions src/resources/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ export function failIfHasValue(
export function standardContentManipulations($: any) {
failIfPresent($, ['ipa', 'bdo']);

handleJmolApplets($);
handleCommandButtons($);

DOM.unwrapInlinedMedia($, 'video');
Expand Down Expand Up @@ -297,9 +298,9 @@ export function standardContentManipulations($: any) {
DOM.rename($, 'mtemp>material', 'p');
DOM.eliminateLevel($, 'mtemp');

// Strip composite-activity if only one child
DOM.rename($, 'composite_activity > instructions', 'p');
// Strip composite-activity entirely if only one child
DOM.eliminateLevel($, 'composite_activity:has(> :only-child)');

DOM.rename($, 'composite_activity', 'group');

// Strip alternatives within feedback/explanation, won't work.
Expand Down Expand Up @@ -343,13 +344,8 @@ export function standardContentManipulations($: any) {
DOM.renameAttribute($, 'video source', 'src', 'url');
DOM.renameAttribute($, 'video', 'type', 'contenttype');
DOM.renameAttribute($, 'audio', 'type', 'audioType');
// video <track> subelement with .vtt subtitle file => torus captions child
DOM.rename($, 'video track[kind="subtitles"]', 'captions');
DOM.renameAttribute($, 'video captions', 'srclang', 'language_code');

DOM.rename($, 'extra', 'popup');
// simplifies handling within popup:
DOM.eliminateLevel($, 'meaning > material');

handleTheorems($);
handleFormulaMathML($);
Expand Down Expand Up @@ -407,6 +403,81 @@ function handleCommandButtons($: any) {
$('command_button').wrap('<p></p>');
}

function handleJmolApplets($: any) {
const jmolWrapper = 'edu.cmu.oli.messaging.applet.jmol.JmolAppletWrapper';

const normalizeScriptValue = (value: string) =>
value
.replace(/[\r\n]+/g, ' ')
.replace(/\s+/g, ' ')
.trim();

$('applet').each((i: any, elem: any) => {
const codeAttr = ($(elem).attr('code') || '').trim();
const isJmol = codeAttr.includes(jmolWrapper);

if (!isJmol) {
return;
}

const idref = $(elem).attr('id') || '';
const widthAttr = $(elem).attr('width');
const heightAttr = $(elem).attr('height');

const params: Record<string, string> = {};

$(elem)
.children('param')
.each((_j: any, p: any) => {
const name = ($(p).attr('name') || '').trim();
if (!name) return;

let raw = '';
if (name === 'load') {
// In legacy use, molecule files were always loaded from webcontent, not public databases.
// These file loads were same-origin as jsmolframe page in legacy, but wind up cross-origin on
// torus (because frame comes from torus superactivity space but data files come from S3). For
// cross-origin loads, jsmol normally calls into a back-end jsmol.php on a specified server to
// avoid CORS problems, but Torus doesn't run this and we don't want to rely on an external server.
// JSMOL supports including a magic ?ALLOWSORIGIN? marker parameter in the URL to tell it
// host allows our origin via CORS, so we always add that to ensure it will do a direct load.
const wbPath = $(p).find('wb\\:path');
const href = wbPath.attr('href');
raw = href !== undefined ? href : $(p).text();
raw = raw + '?ALLOWSORIGIN?';
} else if (name === 'script') {
raw = normalizeScriptValue($(p).text());
} else {
raw = $(p).text().trim();
}

params[name] = raw;
});

const encodedParams = encodeURIComponent(JSON.stringify(params));
const src =
'/superactivity/jsmol/jmolframe.html' +
`?idref=${encodeURIComponent(idref)}&amp;params=${encodedParams}`;

const iframe = $('<iframe></iframe>');
if (idref) {
iframe.attr('id', idref);
}
iframe.attr('src', src);
iframe.attr('scrolling', 'no');
iframe.attr('frameborder', '0');

if (widthAttr !== undefined) {
iframe.attr('width', String(Number(widthAttr) + 5));
}
if (heightAttr !== undefined) {
iframe.attr('height', String(Number(heightAttr) + 5));
}

$(elem).replaceWith(iframe);
});
}

// We don't want empty caption tags ie <caption /> to be expanded to an empty caption with <p></p> in it for iframes (maybe others?)
// This will remove those before they get expanded
function stripEmptyCaptions($: any, selector: string) {
Expand Down
15 changes: 11 additions & 4 deletions src/utils/xml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,17 @@ const xmlParser = require('./parser');
export type TagVisitor = (tag: string, attributes: unknown) => void;
export type ClosingTagVisitor = (tag: string) => void;

// one course left commented out old DOCTYPE when upgrading, must ignore
function stripCommentedDoctype(content: string): string {
return content.replace(/<!--\s*<!DOCTYPE[\s\S]*?-->/gi, '');
}

function getPastDocType(content: string): string {
if (content.indexOf('DOCTYPE') !== -1) {
return content.substr(content.indexOf('>', content.indexOf('DOCTYPE')) + 1);
const cleaned = stripCommentedDoctype(content);
if (cleaned.indexOf('DOCTYPE') !== -1) {
return cleaned.substr(cleaned.indexOf('>', cleaned.indexOf('DOCTYPE')) + 1);
}
return content;
return cleaned;
}

export function isBlockElement(name: string) {
Expand Down Expand Up @@ -749,7 +755,8 @@ export function toJSON(
export function rootTag(file: string): Promise<string> {
return new Promise((resolve, _reject) => {
const content: string = fs.readFileSync(file, 'utf-8');
const dtd = content.substr(content.indexOf('<!DOCTYPE'));
const cleaned = stripCommentedDoctype(content);
const dtd = cleaned.substr(cleaned.indexOf('<!DOCTYPE'));
// normalize whitespace for ease of pattern matching in case split over lines
resolve(dtd.substr(0, dtd.indexOf('>') + 1).replace(/\s+/g, ' '));
});
Expand Down
1 change: 1 addition & 0 deletions test/content/webcontent/jmol/jmol/pdbs/methane_mir.pdb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
REMARK dummy test fixture for JMOL webcontent bundle resolution
23 changes: 23 additions & 0 deletions test/content/x-oli-workbook_page/jmol-applet.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE workbook_page PUBLIC "-//Carnegie Mellon University//DTD Workbook Page MathML 3.8//EN" "http://oli.web.cmu.edu/dtd/oli_workbook_page_mathml_3_8.dtd">
<workbook_page xmlns:bib="http://bibtexml.sf.net/"
xmlns:cmd="http://oli.web.cmu.edu/content/metadata/2.1/"
xmlns:m="http://www.w3.org/1998/Math/MathML" xmlns:pref="http://oli.web.cmu.edu/preferences/"
xmlns:theme="http://oli.web.cmu.edu/presentation/"
xmlns:wb="http://oli.web.cmu.edu/activity/workbook/" id="jmol-test">
<head>
<title>JMOL Test</title>
</head>
<body>
<applet id="methane2"
code="edu.cmu.oli.messaging.applet.jmol.JmolAppletWrapper"
height="125" width="125">
<param name="progressbar">true</param>
<param name="load">
<wb:path href="../webcontent/jmol/jmol/pdbs/methane_mir.pdb" />
</param>
<param name="script">set background white;
color cpk; wireframe 0.2;</param>
</applet>
</body>
</workbook_page>
59 changes: 59 additions & 0 deletions test/jmol-applet-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import * as path from 'path';
import { MediaSummary } from 'src/media';
import { ProjectSummary } from 'src/project';
import { convert } from 'src/convert';

const mediaSummary: MediaSummary = {
mediaItems: {},
missing: [],
urlPrefix: 'https://example.com/media',
downloadRemote: false,
flattenedNames: {},
webContentBundle: {
name: 'test',
url: 'https://example.com/media/bundles/test',
items: [],
totalSize: 0,
},
};

const projectSummary = new ProjectSummary(
path.resolve('./test'),
'',
'',
mediaSummary
);

describe('jmol applet', () => {
let results: any = {};

beforeAll(async () => {
return convert(
projectSummary,
'./test/content/x-oli-workbook_page/jmol-applet.xml',
true
).then((r) => {
results = r;
});
});

test('should convert jmol applet to iframe with encoded params', () => {
const content = results[0].content.model[0].children;
const [iframe] = content;

const params = {
progressbar: 'true',
load: 'https://example.com/media/bundles/test/webcontent/jmol/jmol/pdbs/methane_mir.pdb?ALLOWSORIGIN?',
script: 'set background white; color cpk; wireframe 0.2;',
};

const expectedSrc =
'/superactivity/jsmol/jmolframe.html' +
`?idref=methane2&params=${encodeURIComponent(JSON.stringify(params))}`;

expect(iframe.type).toBe('iframe');
expect(iframe.src).toBe(expectedSrc);
expect(iframe.width).toBe('130');
expect(iframe.height).toBe('130');
});
});
Loading