feat: implement automation feature
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import type { ExerciseQuestionRendererProps } from '@cio/question-types';
|
||||
import { sanitizeHtml } from '../../../../tools/sanitize';
|
||||
import { SafeHtmlContent } from '../../../safe-html-content';
|
||||
|
||||
import { getAnswerForQuestion, getSubmissionLabel } from '../submission-utils';
|
||||
|
||||
@@ -52,7 +53,7 @@
|
||||
{#each responseRows as row, index (`${index}-${row.responses}`)}
|
||||
<div class="ui:flex ui:items-start ui:justify-between ui:gap-3 ui:rounded-md ui:border ui:px-3 ui:py-2">
|
||||
<div class="ui:min-w-0 ui:flex-1 ui:break-words ui:text-sm">
|
||||
{@html row.html}
|
||||
<SafeHtmlContent content={row.html} />
|
||||
</div>
|
||||
<span class="ui:text-muted-foreground ui:mt-0.5 ui:shrink-0 ui:text-xs">{row.responses}</span>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { default as SafeHtmlContent } from './safe-html-content.svelte';
|
||||
@@ -0,0 +1,38 @@
|
||||
<script lang="ts">
|
||||
import { splitHtmlAndSvg, type ContentSegment } from '../../tools/sanitize';
|
||||
|
||||
interface Props {
|
||||
content: string;
|
||||
}
|
||||
|
||||
let { content }: Props = $props();
|
||||
|
||||
const segments: ContentSegment[] = $derived(splitHtmlAndSvg(content));
|
||||
|
||||
function svgDimensions(svg: string): { width: string; height: string } {
|
||||
const widthMatch = svg.match(/\bwidth\s*=\s*["'](\d+)/i);
|
||||
const heightMatch = svg.match(/\bheight\s*=\s*["'](\d+)/i);
|
||||
return {
|
||||
width: widthMatch ? `${widthMatch[1]}px` : '100%',
|
||||
height: heightMatch ? `${heightMatch[1]}px` : '150px'
|
||||
};
|
||||
}
|
||||
|
||||
function svgSrcdoc(rawSvg: string): string {
|
||||
return `<!DOCTYPE html><html><head><style>body{margin:0;display:flex;justify-content:center}
|
||||
</style></head><body>${rawSvg}</body></html>`;
|
||||
}
|
||||
</script>
|
||||
|
||||
{#each segments as segment}
|
||||
{#if segment.type === 'html'}
|
||||
{@html segment.content}
|
||||
{:else}
|
||||
{@const dims = svgDimensions(segment.content)}
|
||||
<iframe
|
||||
sandbox=""
|
||||
srcdoc={svgSrcdoc(segment.content)}
|
||||
title="Embedded diagram"
|
||||
style="border:none;overflow:hidden;width:{dims.width};height:{dims.height}"
|
||||
></iframe>
|
||||
{/if}
|
||||
File diff suppressed because one or more lines are too long
@@ -150,6 +150,37 @@ function fallbackSanitize(html: string): string {
|
||||
return html;
|
||||
}
|
||||
|
||||
export type ContentSegment = { type: 'html'; content: string } | { type: 'svg'; content: string };
|
||||
|
||||
/**
|
||||
* Splits HTML into safe-HTML segments and raw SVG segments.
|
||||
* HTML segments are sanitized; SVG segments are left raw so the caller
|
||||
* can render them inside a sandboxed iframe (sandbox="") where scripts,
|
||||
* forms, and cookie access are all disabled by the browser.
|
||||
*/
|
||||
export function splitHtmlAndSvg(html: string): ContentSegment[] {
|
||||
if (typeof html !== 'string' || !html) return [];
|
||||
|
||||
const segments: ContentSegment[] = [];
|
||||
const svgRegex = /<svg\b[^>]*>[\s\S]*?<\/svg>/gi;
|
||||
let lastIndex = 0;
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
while ((match = svgRegex.exec(html)) !== null) {
|
||||
if (match.index > lastIndex) {
|
||||
segments.push({ type: 'html', content: sanitizeHtml(html.slice(lastIndex, match.index)) });
|
||||
}
|
||||
segments.push({ type: 'svg', content: match[0] });
|
||||
lastIndex = svgRegex.lastIndex;
|
||||
}
|
||||
|
||||
if (lastIndex < html.length) {
|
||||
segments.push({ type: 'html', content: sanitizeHtml(html.slice(lastIndex)) });
|
||||
}
|
||||
|
||||
return segments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Strips all HTML tags and returns plain text
|
||||
* Use this when you only need the text content
|
||||
|
||||
Reference in New Issue
Block a user