Migrate to Parcel and Typescript.

This commit is contained in:
projectmoon 2023-01-03 12:12:34 +01:00
parent 10c31561fd
commit 4a761fb031
14 changed files with 1997 additions and 23 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
.parcel-cache/
dist/
node_modules/

9
.parcelrc Normal file
View File

@ -0,0 +1,9 @@
{
"extends": "@parcel/config-default",
"transformers": {
"*.{ts,tsx}": ["@parcel/transformer-typescript-tsc"]
},
"validators": {
"*.{ts,tsx}": ["@parcel/validator-typescript"]
}
}

View File

@ -75,14 +75,17 @@ function importStylesheet(xsl) {
}
async function createXSLTProcessor() {
return fetch("/MessageLog.xsl")
return fetch(new URL("/MessageLog.xsl"))
.then(resp => {
if (resp.ok)
return resp.text();
else
throw new Error(resp.status + ' ' + resp.statusText);
})
.then(str => new DOMParser().parseFromString(str, "text/xml"))
.then(str => {
console.log(str);
return new DOMParser().parseFromString(str, "text/xml");
})
.then(importStylesheet)
.catch(cause => {
throw new Error('Could not load XSL stylesheet', { cause });

View File

@ -1,10 +0,0 @@
{
"compilerOptions": {
"target": "es2017",
"allowSyntheticDefaultImports": true,
"noEmit": true,
"checkJs": true,
"jsx": "react",
"lib": [ "dom", "es2017" ]
}
}

22
package.json Normal file
View File

@ -0,0 +1,22 @@
{
"name": "msn-viewer",
"source": "src/index.html",
"browserslist": "> 0.5%, last 2 versions, not dead",
"scripts": {
"start": "parcel",
"build": "parcel build"
},
"devDependencies": {
"@parcel/transformer-inline-string": "2.8.2",
"@parcel/transformer-sass": "2.8.2",
"@parcel/transformer-typescript-tsc": "^2.8.2",
"@parcel/validator-typescript": "^2.8.2",
"parcel": "^2.8.2",
"typescript": ">=3.0.0"
},
"dependencies": {
"@popperjs/core": "^2.11.6",
"@types/bootstrap": "^5.2.3",
"bootstrap": "^5.2"
}
}

View File

@ -4,11 +4,7 @@
<title>🔥 MSN Viewer</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta charset="UTF-8" />
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-GLhlTQ8iRABdZLl6O3oVMWSktQOp6b7In1Zl3/Jr59b6EGGoI1aFkw7cmDA6j6gD" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.3/font/bootstrap-icons.css">
<link rel="stylesheet" href="index.css" />
<script type="text/javascript" src="index.js"></script>
<link rel="stylesheet" href="./scss/index.scss" />
</head>
<body>
@ -27,7 +23,7 @@
</div>
<div id="real-body" class="d-none">
<nav id="main-navbar" class="navbar px-0 sticky-top navbar-expand-lg navbar-light bg-body-tertiary">
<nav id="main-navbar" class="navbar px-0 sticky-top navbar-expand-lg bg-light">
<div class="container-fluid">
<a class="navbar-brand" href="#">
🔥 MSN Viewer
@ -159,10 +155,6 @@
</div>
</div>
</div>
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"
integrity="sha384-w76AqPfDkMBDXo30jS1Sgez6pr3x5MlQ1ZAGC+nuZB+EYdgRZgiwxhTBTkF7CXvN"
crossorigin="anonymous"></script>
<script type="module" src="index.ts"></script>
</body>
</html>

163
src/index.js Normal file
View File

@ -0,0 +1,163 @@
import * as bootstrap from 'bootstrap';
import xsl from 'bundle-text:./MessgeLog.xsl';
function showApplication(toggle) {
if (toggle) {
document.getElementById('real-body').classList.remove('d-none');
}
else {
document.getElementById('real-body').classList.add('d-none');
}
}
function showLoadingIndicator(toggle) {
if (toggle) {
document.getElementById('loading').classList.remove('d-none');
}
else {
document.getElementById('loading').classList.add('d-none');
}
}
function displayError(querySelector, category, ex) {
console.error(ex);
const errorDiv = document.querySelector(querySelector);
const errorHeading = errorDiv.querySelector('.alert-heading');
let message = ex.message;
if (ex.hasOwnProperty('cause')) {
if (ex.cause.hasOwnProperty('message')) {
// @ts-ignore
message += ': ' + ex.cause.message;
}
else {
message += ex.cause;
}
}
//clear all error text, but not heading
const errorMessages = errorDiv.querySelectorAll('span');
errorMessages.forEach(message => errorDiv.removeChild(message));
//add new message and show the error alert
const errorSpan = document.createElement('span');
errorSpan.innerText = message;
errorDiv.appendChild(errorSpan);
errorHeading.innerText = category;
errorDiv.classList.remove('d-none');
}
function displayFatalError(category, ex) {
showApplication(false);
showLoadingIndicator(false);
displayError('#fatal-error', category, ex);
}
function displayNonFatalError(category, ex) {
showApplication(true);
showLoadingIndicator(false);
displayError('#display-backup-error', category, ex);
}
function hideErrors() {
document
.querySelectorAll('#fatal-error, #display-backup-error')
.forEach(el => el.classList.add('d-none'));
}
function importStylesheet(xsl) {
//Firefox does not seem to report XML parsing errors into the
//exception tree. So we catch here, log, and return some friendly
//error.
try {
const xsltProcessor = new XSLTProcessor();
xsltProcessor.importStylesheet(xsl);
return xsltProcessor;
}
catch (cause) {
console.error(cause);
throw new Error('Stylesheet parsing failed', { cause });
}
}
async function createXSLTProcessor() {
const doc = new DOMParser().parseFromString(xsl, "text/xml");
return Promise.resolve(importStylesheet(doc));
// return fetch(new URL("./MessageLog.xsl", import.meta.url))
// .then(resp => {
// if (resp.ok)
// return resp.text();
// else
// throw new Error(resp.status + ' ' + resp.statusText);
// })
// .then(str => new DOMParser().parseFromString(str, "text/xml"))
// .then(importStylesheet)
// .catch(cause => {
// throw new Error('Could not load XSL stylesheet', { cause });
// });
}
function removeChildren(element) {
while (element.hasChildNodes()) {
element.removeChild(element.lastChild);
}
}
function checkOverflow(elem) {
const elemHeight = elem.scrollHeight;
const parentHeight = elem.offsetHeight;
return elemHeight > parentHeight;
}
function processFragment() {
document
.querySelectorAll('[data-bs-toggle="popover"]')
.forEach(popover => new bootstrap.Popover(popover));
document.querySelectorAll('.message-content div').forEach(div => {
if (checkOverflow(div)) {
div.parentElement.classList.add('overflow-icon');
}
});
}
function displayBackup(xsltProcessor, file) {
hideErrors();
//document.getElementById('intro-card').classList.add('d-none');
const chatDisplay = document.getElementById("chat-display");
removeChildren(chatDisplay);
file.text().then(xmlText => {
document.getElementById('currently-viewing').innerText = file.name;
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(xmlText, "text/xml");
const errorNode = xmlDoc.querySelector('parsererror');
if (!errorNode) {
try {
const fragment = xsltProcessor.transformToFragment(xmlDoc, document);
chatDisplay.appendChild(fragment);
processFragment();
}
catch (e) {
displayNonFatalError('Could not load backup', e);
}
}
else {
const err = new Error(errorNode.firstChild.nodeValue);
displayNonFatalError('Could not load backup', err);
}
});
}
function initEvents(xsltProcessor) {
const selectFileButtons = document.querySelectorAll("#backup-file-button, #backup-file-button-card");
const fileSelector = document.getElementById("backup-file");
selectFileButtons.forEach(button => button.addEventListener("click", e => {
fileSelector.click();
e.preventDefault();
}));
fileSelector.addEventListener("change", async (e) => {
const target = e.currentTarget;
const file = target.files[0];
displayBackup(xsltProcessor, file);
});
}
window.addEventListener('resize', () => {
if (window.innerWidth <= 500) {
document.querySelector('#chat-display table').classList.add('table-sm');
}
else {
document.querySelector('#chat-display table').classList.remove('table-sm');
}
});
window.addEventListener('DOMContentLoaded', async () => {
createXSLTProcessor()
.then(initEvents)
.then(_ => {
showLoadingIndicator(false);
showApplication(true);
})
.catch(ex => displayFatalError('Initialiation Failed', ex));
});

171
src/index.ts Normal file
View File

@ -0,0 +1,171 @@
import * as bootstrap from 'bootstrap';
import xsl from 'bundle-text:./MessageLog.xsl';
import * as ui from './ui';
function showApplication(toggle: boolean) {
if (toggle) {
ui.realBody.classList.remove('d-none');
} else {
ui.realBody.classList.add('d-none');
}
}
function showLoadingIndicator(toggle: boolean) {
if (toggle) {
ui.loadingIndicator.classList.remove('d-none');
} else {
ui.loadingIndicator.classList.add('d-none');
}
}
function displayError(errorDiv: HTMLElement, category: string, ex: Error) {
console.error(ex);
const errorHeading = errorDiv.querySelector('.alert-heading') as HTMLElement | null;
//clear all error text, but not heading
const errorMessages = errorDiv.querySelectorAll('span');
errorMessages.forEach(message => errorDiv.removeChild(message));
//add new message and show the error alert
const errorSpan = document.createElement('span');
errorSpan.innerText = ex.message;
errorDiv.appendChild(errorSpan);
if (errorHeading) {
errorHeading.innerText = category;
}
errorDiv.classList.remove('d-none');
}
function displayFatalError(category: string, ex: Error) {
showApplication(false);
showLoadingIndicator(false);
displayError(ui.fatalError, category, ex);
}
function displayNonFatalError(category: string, ex: Error) {
showApplication(true);
showLoadingIndicator(false);
displayError(ui.nonFatalError, category, ex);
}
function hideErrors() {
document
.querySelectorAll('#fatal-error, #display-backup-error')
.forEach(el => el.classList.add('d-none'));
}
function importStylesheet(xsl: Node) {
//Firefox does not seem to report XML parsing errors into the
//exception tree. So we catch here, log, and return some friendly
//error.
try {
const xsltProcessor = new XSLTProcessor();
xsltProcessor.importStylesheet(xsl);
return xsltProcessor;
} catch (cause) {
console.error(cause);
throw new Error('Stylesheet parsing failed');
}
}
async function createXSLTProcessor() {
const doc = new DOMParser().parseFromString(xsl, "text/xml");
return Promise.resolve(importStylesheet(doc));
// return fetch(new URL("./MessageLog.xsl", import.meta.url))
// .then(resp => {
// if (resp.ok)
// return resp.text();
// else
// throw new Error(resp.status + ' ' + resp.statusText);
// })
// .then(str => new DOMParser().parseFromString(str, "text/xml"))
// .then(importStylesheet)
// .catch(cause => {
// throw new Error('Could not load XSL stylesheet', { cause });
// });
}
function removeChildren(element: HTMLElement) {
while (element.hasChildNodes() && element.lastChild != null) {
element.removeChild(element.lastChild);
}
}
function checkOverflow(elem: HTMLElement) {
const elemHeight = elem.scrollHeight;
const parentHeight = elem.offsetHeight;
return elemHeight > parentHeight;
}
function processFragment() {
document
.querySelectorAll('[data-bs-toggle="popover"]')
.forEach(popover => new bootstrap.Popover(popover));
document.querySelectorAll('.message-content div').forEach(div => {
if (checkOverflow(div as HTMLElement)) {
div.parentElement?.classList.add('overflow-icon');
}
});
}
function displayBackup(xsltProcessor: XSLTProcessor, file: File) {
hideErrors();
removeChildren(ui.chatDisplay);
file.text().then(xmlText => {
ui.currentlyViewing.innerText = file.name;
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(xmlText, "text/xml");
const errorNode = xmlDoc.querySelector('parsererror');
if (!errorNode) {
try {
const fragment = xsltProcessor.transformToFragment(xmlDoc, document);
ui.chatDisplay.appendChild(fragment);
processFragment();
}
catch (e) {
if (e instanceof Error) {
displayNonFatalError('Could not load backup', e);
}
}
} else {
const err = new Error(errorNode.firstChild?.nodeValue || '');
displayNonFatalError('Could not load backup', err);
}
});
}
function initEvents(xsltProcessor: XSLTProcessor) {
ui.fileSelector.addEventListener("change", async (e) => {
const target = e.currentTarget as HTMLInputElement | null;
const file = target?.files?.[0];
if (file) {
displayBackup(xsltProcessor, file);
} else {
displayNonFatalError('Could not load file', new Error('No file found.'));
}
});
}
window.addEventListener('DOMContentLoaded', async () => {
window.addEventListener('resize', () => {
if (window.innerWidth <= 500) {
ui.chatDisplay.querySelector('table')?.classList.add('table-sm');
} else {
ui.chatDisplay.querySelector('table')?.classList.remove('table-sm');
}
});
createXSLTProcessor()
.then(initEvents)
.then(_ => {
showLoadingIndicator(false);
showApplication(true);
})
.catch(ex => displayFatalError('Initialiation Failed', ex));
});

5
src/parcel.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
// Enable typescript completion for bundle-text inlining.
declare module 'bundle-text:*' {
const value: string;
export default value;
}

View File

@ -1,3 +1,5 @@
@import "~bootstrap/scss/bootstrap";
table {
/* table-layout: fixed; */
}

16
src/tsconfig.json Normal file
View File

@ -0,0 +1,16 @@
{
"compilerOptions": {
"strict": true,
"experimentalDecorators": true,
"target": "es6",
"module": "es6",
"lib": [ "dom", "es6" ],
"jsxImportSource": "preact",
"isolatedModules": true,
"moduleResolution": "node",
"baseUrl": ".",
"paths": {
"~*": ["./*"]
}
}
}

9
src/ui.ts Normal file
View File

@ -0,0 +1,9 @@
//All of these can ignore maybe-null errors, as the script runs at the bottom of the page.
//So DOM is guaranteed to exist.
export const realBody: HTMLElement = document.getElementById('real-body')!;
export const loadingIndicator: HTMLElement = document.getElementById('loading')!;
export const fatalError: HTMLElement = document.getElementById('fatal-error')!;
export const nonFatalError: HTMLElement = document.getElementById('display-backup-error')!;
export const chatDisplay: HTMLElement = document.getElementById('chat-display')!;
export const fileSelector: HTMLInputElement = document.getElementById('backup-file')! as HTMLInputElement;
export const currentlyViewing: HTMLElement = document.getElementById('currently-viewing')!;

1588
yarn.lock Normal file

File diff suppressed because it is too large Load Diff