This commit is contained in:
@@ -1,16 +0,0 @@
|
||||
import {queryElems} from '../utils/dom.ts';
|
||||
|
||||
export async function initMarkupRenderAsciicast(elMarkup: HTMLElement): Promise<void> {
|
||||
queryElems(elMarkup, '.asciinema-player-container', async (el) => {
|
||||
const [player] = await Promise.all([
|
||||
import('asciinema-player'),
|
||||
import('asciinema-player/dist/bundle/asciinema-player.css'),
|
||||
]);
|
||||
|
||||
player.create(el.getAttribute('data-asciinema-player-src')!, el, {
|
||||
// poster (a preview frame) to display until the playback is started.
|
||||
// Set it to 1 hour (also means the end if the video is shorter) to make the preview frame show more.
|
||||
poster: 'npt:1:0:0',
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
export function displayError(el: Element, err: Error): void {
|
||||
import {errorMessage} from '../modules/errors.ts';
|
||||
|
||||
export function displayError(el: Element, err: unknown): void {
|
||||
el.classList.remove('is-loading');
|
||||
const errorNode = document.createElement('pre');
|
||||
errorNode.setAttribute('class', 'ui message error markup-block-error');
|
||||
errorNode.textContent = err.message || String(err);
|
||||
errorNode.textContent = errorMessage(err);
|
||||
el.before(errorNode);
|
||||
el.setAttribute('data-render-done', 'true');
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import {initMarkupCodeMermaid} from './mermaid.ts';
|
||||
import {initMarkupCodeMath} from './math.ts';
|
||||
import {initMarkupCodeCopy} from './codecopy.ts';
|
||||
import {initMarkupRenderAsciicast} from './asciicast.ts';
|
||||
import {initMarkupTasklist} from './tasklist.ts';
|
||||
import {registerGlobalInitFunc, registerGlobalSelectorFunc} from '../modules/observer.ts';
|
||||
import {initExternalRenderIframe} from './render-iframe.ts';
|
||||
import {initMarkupRefIssue} from './refissue.ts';
|
||||
import {toggleElemClass} from '../utils/dom.ts';
|
||||
|
||||
// code that runs for all markup content
|
||||
@@ -25,7 +23,5 @@ export function initMarkupContent(): void {
|
||||
initMarkupTasklist(el);
|
||||
initMarkupCodeMermaid(el);
|
||||
initMarkupCodeMath(el);
|
||||
initMarkupRenderAsciicast(el);
|
||||
initMarkupRefIssue(el);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -18,15 +18,9 @@ function prepareProcessors(ctx:ProcessorContext): Processors {
|
||||
const level = parseInt(el.tagName.slice(1));
|
||||
el.textContent = `${'#'.repeat(level)} ${el.textContent.trim()}`;
|
||||
},
|
||||
STRONG(el: HTMLElement) {
|
||||
return `**${el.textContent}**`;
|
||||
},
|
||||
EM(el: HTMLElement) {
|
||||
return `_${el.textContent}_`;
|
||||
},
|
||||
DEL(el: HTMLElement) {
|
||||
return `~~${el.textContent}~~`;
|
||||
},
|
||||
STRONG: (el: HTMLElement) => `**${el.textContent}**`,
|
||||
EM: (el: HTMLElement) => `_${el.textContent}_`,
|
||||
DEL: (el: HTMLElement) => `~~${el.textContent}~~`,
|
||||
A(el: HTMLElement) {
|
||||
const text = el.textContent || 'link';
|
||||
const href = el.getAttribute('href');
|
||||
@@ -62,9 +56,7 @@ function prepareProcessors(ctx:ProcessorContext): Processors {
|
||||
el.textContent = `${' '.repeat(nestingIdentLevel * 4)}${bullet}${el.textContent}${ctx.elementIsLast ? '' : '\n'}`;
|
||||
return el;
|
||||
},
|
||||
INPUT(el: HTMLElement) {
|
||||
return (el as HTMLInputElement).checked ? '[x] ' : '[ ] ';
|
||||
},
|
||||
INPUT: (el: HTMLElement) => (el as HTMLInputElement).checked ? '[x] ' : '[ ] ',
|
||||
CODE(el: HTMLElement) {
|
||||
const text = el.textContent;
|
||||
if (el.parentNode && (el.parentNode as HTMLElement).tagName === 'PRE') {
|
||||
@@ -86,8 +78,8 @@ function prepareProcessors(ctx:ProcessorContext): Processors {
|
||||
|
||||
function processElement(ctx :ProcessorContext, processors: Processors, el: HTMLElement): string | void {
|
||||
if (el.hasAttribute('data-markdown-generated-content')) return el.textContent;
|
||||
if (el.tagName === 'A' && el.children.length === 1 && el.children[0].tagName === 'IMG') {
|
||||
return processElement(ctx, processors, el.children[0] as HTMLElement);
|
||||
if (el.tagName === 'A' && el.children.length === 1 && el.firstElementChild!.tagName === 'IMG') {
|
||||
return processElement(ctx, processors, el.firstElementChild as HTMLElement);
|
||||
}
|
||||
|
||||
const isListContainer = el.tagName === 'OL' || el.tagName === 'UL';
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import {queryElems} from '../utils/dom.ts';
|
||||
import {parseIssueHref} from '../utils.ts';
|
||||
import {createApp} from 'vue';
|
||||
import {createTippy, getAttachedTippyInstance} from '../modules/tippy.ts';
|
||||
|
||||
export function initMarkupRefIssue(el: HTMLElement) {
|
||||
queryElems(el, '.ref-issue', (el) => {
|
||||
el.addEventListener('mouseenter', showMarkupRefIssuePopup);
|
||||
el.addEventListener('focus', showMarkupRefIssuePopup);
|
||||
});
|
||||
}
|
||||
|
||||
function showMarkupRefIssuePopup(e: MouseEvent | FocusEvent) {
|
||||
const refIssue = e.currentTarget as HTMLElement;
|
||||
if (getAttachedTippyInstance(refIssue)) return;
|
||||
if (refIssue.classList.contains('ref-external-issue')) return;
|
||||
|
||||
const issuePathInfo = parseIssueHref(refIssue.getAttribute('href')!);
|
||||
if (!issuePathInfo.ownerName) return;
|
||||
|
||||
const el = document.createElement('div');
|
||||
const onShowAsync = async () => {
|
||||
const {default: ContextPopup} = await import('../components/ContextPopup.vue');
|
||||
const view = createApp(ContextPopup, {
|
||||
// backend: GetIssueInfo
|
||||
loadIssueInfoUrl: `${window.config.appSubUrl}/${issuePathInfo.ownerName}/${issuePathInfo.repoName}/issues/${issuePathInfo.indexString}/info`,
|
||||
});
|
||||
view.mount(el);
|
||||
};
|
||||
const tippy = createTippy(refIssue, {
|
||||
theme: 'default',
|
||||
content: el,
|
||||
trigger: 'mouseenter focus',
|
||||
placement: 'top-start',
|
||||
interactive: true,
|
||||
role: 'dialog',
|
||||
interactiveBorder: 5,
|
||||
// onHide() { return false }, // help to keep the popup and debug the layout
|
||||
onShow: () => { onShowAsync() },
|
||||
});
|
||||
tippy.show();
|
||||
}
|
||||
@@ -29,7 +29,6 @@ describe('navigateToIframeLink', () => {
|
||||
|
||||
test('unsafe links', () => {
|
||||
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
||||
window.location.href = 'http://localhost:3000/';
|
||||
|
||||
// eslint-disable-next-line no-script-url
|
||||
navigateToIframeLink('javascript:void(0);', '_blank');
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import {generateElemId} from '../utils/dom.ts';
|
||||
import {errorMessage} from '../modules/errors.ts';
|
||||
import {isDarkTheme} from '../utils.ts';
|
||||
import {GET} from '../modules/fetch.ts';
|
||||
|
||||
function safeRenderIframeLink(link: any): string | null {
|
||||
try {
|
||||
const url = new URL(`${link}`, window.location.href);
|
||||
const url = new URL(link, window.location.href);
|
||||
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
||||
console.error(`Unsupported link protocol: ${link}`);
|
||||
return null;
|
||||
}
|
||||
return url.href;
|
||||
} catch (e) {
|
||||
console.error(`Failed to parse link: ${link}, error: ${e}`);
|
||||
console.error(`Failed to parse link: ${link}, error: ${errorMessage(e)}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -64,9 +64,31 @@ export async function initExternalRenderIframe(iframe: HTMLIFrameElement) {
|
||||
u.searchParams.set('gitea-iframe-id', iframe.id);
|
||||
u.searchParams.set('gitea-iframe-bgcolor', getRealBackgroundColor(iframe));
|
||||
|
||||
// It must use "srcdoc" here, because our backend always sends CSP sandbox directive for the rendered content
|
||||
// (to protect from XSS risks), so we can't use "src" to load the content directly, otherwise there will be console errors like:
|
||||
// Unsafe attempt to load URL http://localhost:3000/test from frame with URL http://localhost:3000/test
|
||||
const resp = await GET(u.href);
|
||||
iframe.srcdoc = await resp.text();
|
||||
// There are 3 kinds of external render modes:
|
||||
// * external frontend render:
|
||||
// * parent page creates iframe, iframe navigates to render page
|
||||
// * render generates frame page with external-render-helper (injected), external-render-frontend and file content (hidden textarea)
|
||||
// * frame page executes external-render-frontend JS code to finds a frontend plugin to render
|
||||
// * external backend render (HTML)
|
||||
// * parent page creates iframe, iframe navigates to render page
|
||||
// * render executes command to generate rendered HTML content with external-render-helper (injected)
|
||||
// * frame page displays the rendered content
|
||||
// * external backend render (non-HTML, e.g.: PDF, image)
|
||||
// * parent page creates iframe, iframe navigates to render page
|
||||
// * render executes command to generate rendered content
|
||||
// * response header is automatically detected from rendered content
|
||||
|
||||
// It must use "src" here, because the frame content should not inherit parent's CSP.
|
||||
// Otherwise, "srcdoc" makes the frame content inherit the parent's CSP,
|
||||
// then some renders like "asciicast (asciinema)" which require "unsafe-eval" won't work.
|
||||
//
|
||||
// When using "src", Chrome can report false-alarm error like:
|
||||
// * Unsafe attempt to load URL http://localhost/owner/repo/render/branch/main/file from frame with URL http://localhost/owner/repo/render/branch/main/file. Domains, protocols and ports must match.
|
||||
// (only for the first time that the developer opens the browser console)
|
||||
// Such error log can also appear even if you access the link "http://.../owner/repo/render/branch/main/file" directly.
|
||||
// Everything just works, it is just a false-alarm caused by Chrome's Developer Tools, so such error log can be ignored.
|
||||
//
|
||||
// Another reason for why "src" is a must: if the render outputs non-HTML contents like PDF or image,
|
||||
// Only "src" can correctly load and display the rendered content, "srcdoc" won't work.
|
||||
iframe.src = u.href;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user