aife-twee2
Version:
159 lines (141 loc) • 4.19 kB
JavaScript
;
const selectors = {
passage: 'tw-passage',
story: 'tw-story',
script: '[role=script]',
stylesheet: '[role=stylesheet]',
storyData: 'tw-storydata',
passageData: 'tw-passagedata',
};
/**
* @param {Element} storyEl
* @param {string} type
* @return {string}
*/
function extractCode(storyEl, type) {
return Array.from(storyEl.querySelectorAll(selectors[type]))
.reduce(
(src, el) => `${src}${el.textContent}\n`,
''
);
}
function extractPassage({attributes, textContent,}) {
const pos = attributes.position.value
.split(',')
.map(Math.floor);
return {
pid: attributes.pid.value, // Again, a one-off id, not a database id.
left: pos[0],
top: pos[1],
width: attributes.width ? parseInt(attributes.width.value) : 100,
height: attributes.height ? parseInt(attributes.height.value) : 100,
tags: attributes.tags.value === '' ? [] : attributes.tags.value.split(/\s+/),
name: attributes.name.value,
text: textContent,
};
}
/**
* Converts a DOM <tw-storydata> element to a story object matching the internals Twine format.
* @param {Element} storyEl
* @param {boolean} forceLastUpdate
* @return {ITwineStory}
*/
function domToObject(storyEl, forceLastUpdate) {
const attribs = storyEl.attributes;
return {
// Important: this is the passage's pid (a one-off id created at
// publish time), *not* a database id.
startPassagePid: attribs.startnode.value,
name: attribs.name.value,
ifid: attribs.ifid.value,
lastUpdate: forceLastUpdate || new Date(),
script: extractCode(storyEl, 'script'),
stylesheet: extractCode(storyEl, 'stylesheet'),
zoom: 1,
passages: Array.from(storyEl.querySelectorAll(selectors.passageData)).map(extractPassage),
format: attribs.format.value,
};
}
/**
* @param {Element} html
* @param {Date} [lastUpdate = new Date()]
* @return {ITwineStory[]}
*/
export function importStory(html, lastUpdate = new Date()) {
const nodes = document.createElement('div');
nodes.innerHTML = html;
return Array.from(
nodes.querySelectorAll(selectors.storyData)
).map((storyEl) => domToObject(storyEl, lastUpdate));
}
/**
* Smooth importing SC2 stories from twinery
* @param {string} format
* @return {string}
*/
function normalizeFormat(format) {
if (format === 'SugarCube 2 (local/offline)') {
return 'SugarCube 2';
} else {
return format;
}
}
/**
* @param {ITwineStory} twineStoryObject
* @return {IStory}
*/
export function convertStory(twineStoryObject) {
const convertedStory = {
title: twineStoryObject.name,
ifid: twineStoryObject.ifid,
passages: twineStoryObject.passages.map(convertPassage),
opened: [],
styleSheet: twineStoryObject.stylesheet,
editStylesheet: false,
script: twineStoryObject.script,
editScript: false,
customHtml: '',
editCustomHtml: false,
editProof: false,
lastEdit: twineStoryObject.lastUpdate,
format: normalizeFormat(twineStoryObject.format),
};
const startingPid = parseInt(twineStoryObject.startPassagePid);
const startingPassage = convertedStory.passages.find(({pid,}) => pid === startingPid);
if (startingPassage) {
startingPassage.starting = true;
}
return convertedStory;
}
/**
* @param {ITwinePassage} twinePassage
* @return {IPassage}
*/
function convertPassage(twinePassage) {
return {
title: twinePassage.name,
text: twinePassage.text,
pid: Number(twinePassage.pid),
tags: twinePassage.tags,
starting: false,
selected: false,
position: {
x: twinePassage.left,
y: twinePassage.top,
},
};
}
function importXml(htmls) {
const stories = [];
htmls.map(importStory).forEach(
(imported) => {
stories.push(
...imported.map(convertStory)
);
}
);
return stories;
}
module.exports = {
importXml,
};