feat: use jump_from_click from typst-ide (#1399)

This commit is contained in:
Myriad-Dreamin 2025-02-25 19:15:05 +08:00 committed by GitHub
parent 791a7f8314
commit 21c6492254
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 226 additions and 314 deletions

View file

@ -22,12 +22,13 @@ use serde::Serialize;
use serde_json::Value as JsonValue;
use sync_lsp::just_ok;
use tinymist_assets::TYPST_PREVIEW_HTML;
use tinymist_project::{ProjectInsId, WorldProvider};
use tinymist_project::{LspWorld, ProjectInsId, WorldProvider};
use tinymist_std::error::IgnoreLogging;
use tinymist_std::typst::TypstDocument;
use tokio::sync::{mpsc, oneshot};
use typst::layout::{Frame, FrameItem, Point, Position};
use typst::layout::{Abs, Frame, FrameItem, Point, Position, Size};
use typst::syntax::{LinkedNode, Source, Span, SyntaxKind};
use typst::visualize::Geometry;
use typst::World;
use typst_preview::{
frontend_html, ControlPlaneMessage, ControlPlaneResponse, ControlPlaneRx, ControlPlaneTx,
@ -94,6 +95,23 @@ impl typst_preview::CompileView for PreviewCompileView {
Some(SourceSpanOffset { span, offset })
}
// todo: use vec2bbox to handle bbox correctly
fn resolve_frame_loc(
&self,
pos: &reflexo::debug_loc::DocumentPosition,
) -> Option<(SourceSpanOffset, SourceSpanOffset)> {
let TypstDocument::Paged(doc) = self.doc()? else {
return None;
};
let world = &self.snap.world;
let page = pos.page_no.checked_sub(1)?;
let page = doc.pages.get(page)?;
let click = Point::new(Abs::pt(pos.x as f64), Abs::pt(pos.y as f64));
jump_from_click(world, &page.frame, click)
}
fn resolve_document_position(&self, loc: Location) -> Vec<Position> {
let world = &self.snap.world;
let Location::Src(src_loc) = loc;
@ -866,6 +884,85 @@ impl Notification for NotifDocumentOutline {
const METHOD: &'static str = "tinymist/documentOutline";
}
/// Determine where to jump to based on a click in a frame.
pub fn jump_from_click(
world: &LspWorld,
frame: &Frame,
click: Point,
) -> Option<(SourceSpanOffset, SourceSpanOffset)> {
// Try to find a link first.
for (pos, item) in frame.items() {
if let FrameItem::Link(_dest, size) = item {
if is_in_rect(*pos, *size, click) {
// todo: url reaction
return None;
}
}
}
// If there's no link, search for a jump target.
for (mut pos, item) in frame.items().rev() {
match item {
FrameItem::Group(group) => {
// TODO: Handle transformation.
if let Some(span) = jump_from_click(world, &group.frame, click - pos) {
return Some(span);
}
}
FrameItem::Text(text) => {
for glyph in &text.glyphs {
let width = glyph.x_advance.at(text.size);
if is_in_rect(
Point::new(pos.x, pos.y - text.size),
Size::new(width, text.size),
click,
) {
let (span, span_offset) = glyph.span;
let mut span_offset = span_offset as usize;
let Some(id) = span.id() else { continue };
let source = world.source(id).ok()?;
let node = source.find(span)?;
if matches!(node.kind(), SyntaxKind::Text | SyntaxKind::MathText)
&& (click.x - pos.x) > width / 2.0
{
span_offset += glyph.range().len();
}
let span_offset = SourceSpanOffset {
span,
offset: span_offset,
};
return Some((span_offset, span_offset));
}
pos.x += width;
}
}
FrameItem::Shape(shape, span) => {
let Geometry::Rect(size) = shape.geometry else {
continue;
};
if is_in_rect(pos, size, click) {
let span = (*span).into();
return Some((span, span));
}
}
FrameItem::Image(_, size, span) if is_in_rect(pos, *size, click) => {
let span = (*span).into();
return Some((span, span));
}
_ => {}
}
}
None
}
/// Find the output location in the document for a cursor position.
fn jump_from_cursor(document: &TypstDocument, source: &Source, cursor: usize) -> Vec<Position> {
let Some(node) = LinkedNode::new(source.root())
@ -933,6 +1030,12 @@ fn find_in_frame(frame: &Frame, span: Span, min_dis: &mut u64, p: &mut Point) ->
None
}
/// Whether a rectangle with the given size at the given position contains the
/// click position.
fn is_in_rect(pos: Point, size: Size, click: Point) -> bool {
pos.x <= click.x && pos.x + size.x >= click.x && pos.y <= click.y && pos.y + size.y >= click.y
}
fn bind_streams(previewer: &mut Previewer, websocket_rx: mpsc::UnboundedReceiver<HyperWebsocket>) {
previewer.start_data_plane(
websocket_rx,

View file

@ -22,6 +22,7 @@ pub enum RenderActorRequest {
RenderIncremental,
EditorResolveSpanRange(Range<SourceSpanOffset>),
WebviewResolveSpan(ResolveSpanRequest),
WebviewResolveFrameLoc(DocumentPosition),
ResolveSourceLoc(ResolveSourceLocRequest),
ChangeCursorPosition(ChangeCursorPositionRequest),
}
@ -34,6 +35,7 @@ impl RenderActorRequest {
Self::EditorResolveSpanRange(_) => false,
Self::WebviewResolveSpan(_) => false,
Self::ResolveSourceLoc(_) => false,
Self::WebviewResolveFrameLoc(_) => false,
Self::ChangeCursorPosition(_) => false,
}
}
@ -94,6 +96,16 @@ impl RenderActor {
self.editor_resolve_span_range(spans.0..spans.1);
}
}
RenderActorRequest::WebviewResolveFrameLoc(frame_loc) => {
log::debug!("RenderActor: resolving WebviewResolveFrameLoc: {frame_loc:?}");
let spans = self.resolve_span_by_frame_loc(&frame_loc);
log::debug!("RenderActor: resolved WebviewResolveSpan: {spans:?}");
// end position is used
if let Some(spans) = spans {
self.editor_resolve_span_range(spans.0..spans.1);
}
}
RenderActorRequest::ResolveSourceLoc(req) => {
log::debug!("RenderActor: resolving ResolveSourceLoc: {req:?}");
@ -283,6 +295,15 @@ impl RenderActor {
Some(())
}
/// Gets the span range of the given frame loc.
pub fn resolve_span_by_frame_loc(
&mut self,
pos: &DocumentPosition,
) -> Option<(SourceSpanOffset, SourceSpanOffset)> {
let view = self.view.read();
view.as_ref()?.resolve_frame_loc(pos)
}
}
pub struct OutlineRenderActor {

View file

@ -156,8 +156,14 @@ impl<
let path = path.into_iter().map(ElementPoint::from).collect::<Vec<_>>();
self.render_sender.send(RenderActorRequest::WebviewResolveSpan(ResolveSpanRequest(path))).log_error("WebViewActor");
};
} else if msg.starts_with("src-point") {
let path = msg.split(' ').nth(1).unwrap();
let path = serde_json::from_str(path);
if let Ok(path) = path {
self.render_sender.send(RenderActorRequest::WebviewResolveFrameLoc(path)).log_error("WebViewActor");
};
} else {
let err = self.webview_websocket_conn.send(Message::Text(format!("error, received unknown message: {}", msg))).await;
let err = self.webview_websocket_conn.send(Message::Text(format!("error, received unknown message: {msg}"))).await;
log::info!("WebviewActor: received unknown message from websocket: {msg} {err:?}");
break;
}

View file

@ -8,6 +8,7 @@ pub use actor::editor::{
};
pub use args::*;
pub use outline::Outline;
use tinymist_std::debug_loc::DocumentPosition;
use tinymist_std::error::IgnoreLogging;
use std::{collections::HashMap, future::Future, path::PathBuf, pin::Pin, sync::Arc};
@ -370,6 +371,14 @@ pub trait CompileView: Send + Sync {
None
}
/// Resolve a physical location in the document.
fn resolve_frame_loc(
&self,
_pos: &DocumentPosition,
) -> Option<(SourceSpanOffset, SourceSpanOffset)> {
None
}
/// Resolve the document position.
fn resolve_document_position(&self, _by: Location) -> Vec<Position> {
vec![]

View file

@ -32,7 +32,7 @@ const CssClassToType = [
] as const;
function castToSourceMappingElement(
elem: Element
elem: Element,
): [SourceMappingType, Element, string] | undefined {
if (elem.classList.length === 0) {
return undefined;
@ -46,7 +46,7 @@ function castToSourceMappingElement(
}
function castToNestSourceMappingElement(
elem: Element
elem: Element,
): [SourceMappingType, Element, string] | undefined {
while (elem) {
const result = castToSourceMappingElement(elem);
@ -63,9 +63,7 @@ function castToNestSourceMappingElement(
return undefined;
}
function castChildrenToSourceMappingElement(
elem: Element
): [SourceMappingType, Element, string][] {
function castChildrenToSourceMappingElement(elem: Element): [SourceMappingType, Element, string][] {
return Array.from(elem.children)
.map(castToNestSourceMappingElement)
.filter((x) => x) as [SourceMappingType, Element, string][];
@ -80,14 +78,11 @@ export function removeSourceMappingHandler(docRoot: HTMLElement) {
}
}
function findIndexOfChild(elem: Element, child: Element) {
const children = castChildrenToSourceMappingElement(elem);
// console.log(elem, "::", children, "=>", child);
return children.findIndex((x) => x[1] === child);
}
export function resolveSourceLeaf(elem: Element, path: ElementPoint[]): [Element, number] | undefined {
const page = elem.getElementsByClassName('typst-page')[0];
export function resolveSourceLeaf(
elem: Element,
path: ElementPoint[],
): [Element, number] | undefined {
const page = elem.getElementsByClassName("typst-page")[0];
let curElem = page;
for (const point of path.slice(1)) {
@ -110,221 +105,68 @@ export function resolveSourceLeaf(elem: Element, path: ElementPoint[]): [Element
return [curElem, 0];
}
// const rotateColors = [
// "green",
// "blue",
// "red",
// "orange",
// "purple",
// "yellow",
// "cyan",
// "magenta",
// ];
function getCharIndex(elem: Element, mouseX: number, mouseY: number) {
let useIndex = 0;
let foundIndex = -1;
const textRect = elem.getBoundingClientRect();
type SelRect = Pick<DOMRect, 'left' | 'right' | 'top' | 'bottom'>;
let previousSelRect: SelRect | undefined = undefined;
const unionRect = (a: SelRect, b?: SelRect) => {
if (!b) {
return a;
}
return {
left: Math.min(a.left, b.left),
top: Math.min(a.top, b.top),
right: Math.max(a.right, b.right),
bottom: Math.max(a.bottom, b.bottom),
};
}
const inRect = (rect: SelRect, x: number, y: number) => {
return rect.left <= x && x <= rect.right &&
rect.top <= y && y <= rect.bottom;
}
const enum TextFlowDirection {
LeftToRight = 0,
RightToLeft = 1,
TopToBottom = 2,
BottomToTop = 3,
};
let textFlowDir = TextFlowDirection.LeftToRight;
const isHorizontalFlow = () => {
return textFlowDir === TextFlowDirection.LeftToRight ||
textFlowDir === TextFlowDirection.RightToLeft;
}
{
let use0: Element = undefined!;
let use1: Element = undefined!;
for (const use of elem.children) {
if (use.tagName !== 'use') {
continue;
}
if (!use0) {
use0 = use;
continue;
}
use1 = use;
break;
}
if (use0 && use1) {
const use0Rect = use0.getBoundingClientRect();
const use1Rect = use1.getBoundingClientRect();
const use0Center = {
x: (use0Rect.left + use0Rect.right) / 2,
y: (use0Rect.top + use0Rect.bottom) / 2,
};
const use1Center = {
x: (use1Rect.left + use1Rect.right) / 2,
y: (use1Rect.top + use1Rect.bottom) / 2,
};
const vec = {
x: use1Center.x - use0Center.x,
y: use1Center.y - use0Center.y,
};
const angle = Math.atan2(vec.y, vec.x);
// console.log('angle', angle);i
if (angle > -Math.PI / 4 && angle < Math.PI / 4) {
textFlowDir = TextFlowDirection.LeftToRight;
} else if (angle < -Math.PI / 4 && angle > -Math.PI * 3 / 4) {
textFlowDir = TextFlowDirection.TopToBottom;
} else if (angle > Math.PI / 4 && angle < Math.PI * 3 / 4) {
textFlowDir = TextFlowDirection.BottomToTop;
} else {
textFlowDir = TextFlowDirection.RightToLeft;
}
}
}
for (const use of elem.children) {
if (use.tagName !== 'use') {
continue;
}
const useRect = use.getBoundingClientRect();
const selRect = isHorizontalFlow() ? {
left: useRect.left,
right: useRect.right,
top: textRect.top,
bottom: textRect.bottom,
} : {
left: textRect.left,
right: textRect.right,
top: useRect.top,
bottom: useRect.bottom,
};
previousSelRect = unionRect(selRect, previousSelRect);
// draw sel rect for debugging
// const selRectElem = document.createElement('div');
// selRectElem.style.position = 'absolute';
// selRectElem.style.left = `${selRect.left}px`;
// selRectElem.style.top = `${selRect.top}px`;
// selRectElem.style.width = `${selRect.right - selRect.left}px`;
// selRectElem.style.height = `${selRect.bottom - selRect.top}px`;
// selRectElem.style.border = `1px solid ${rotateColors[useIndex % rotateColors.length]}`;
// selRectElem.style.zIndex = '100';
// document.body.appendChild(selRectElem);
// console.log(textRect, selRect);
// set index to end range of this char
useIndex++;
if (inRect(selRect, mouseX, mouseY)) {
foundIndex = useIndex;
} else if (previousSelRect) { // may fallback to space in between chars
if (inRect(previousSelRect, mouseX, mouseY)) {
foundIndex = useIndex - 1;
previousSelRect = selRect;
}
}
}
return foundIndex;
}
export function installEditorJumpToHandler(docRoot: HTMLElement) {
const collectElementPath = async (event: MouseEvent, elem: Element) => {
const visitChain: [SourceMappingType, Element, string][] = [];
const resolveFrameLoc = async (event: MouseEvent, elem: Element) => {
const x = event.clientX;
const y = event.clientY;
let mayPageElem: [SourceMappingType, Element, string] | undefined = undefined;
while (elem) {
let srcElem = castToSourceMappingElement(elem);
if (srcElem) {
if (srcElem[0] === SourceMappingType.CharIndex) {
const textElem = elem.parentElement?.parentElement?.parentElement!;
let foundIndex = -1;
if (textElem) {
foundIndex = getCharIndex(textElem, event.clientX, event.clientY);
}
if (foundIndex !== -1) {
(srcElem[1] as any) = foundIndex;
visitChain.push(srcElem);
}
} else {
visitChain.push(srcElem);
}
mayPageElem = castToSourceMappingElement(elem);
if (mayPageElem && mayPageElem[0] === SourceMappingType.Page) {
break;
}
if (elem === docRoot) {
break;
return;
}
elem = elem.parentElement!;
}
if (visitChain.length === 0) {
if (!mayPageElem) {
return undefined;
}
// console.log('visitChain', visitChain);
const pageElem = mayPageElem[1];
console.log(mayPageElem, pageElem);
let startIdx = 1;
if (visitChain.length >= 1 && visitChain[0][0] === SourceMappingType.CharIndex) {
startIdx = 2;
}
for (let idx = startIdx; idx < visitChain.length; idx++) {
if (visitChain[idx - 1][0] === SourceMappingType.CharIndex) {
throw new Error("unexpected");
}
const pageRect = pageElem.getBoundingClientRect();
const pageX = x - pageRect.left;
const pageY = y - pageRect.top;
const childIdx = findIndexOfChild(
visitChain[idx][1],
visitChain[idx - 1][1]
);
if (childIdx < 0) {
return undefined;
}
(visitChain[idx - 1][1] as any) = childIdx;
}
const xPercent = pageX / pageRect.width;
const yPercent = pageY / pageRect.height;
const pageNumber = pageElem.getAttribute("data-page-number")!;
const dataWidthS = pageElem.getAttribute("data-page-width")!;
const dataHeightS = pageElem.getAttribute("data-page-height")!;
visitChain.reverse();
console.log(pageNumber, dataWidthS, dataHeightS);
const pg = visitChain[0];
if (pg[0] !== SourceMappingType.Page) {
if (!pageNumber || !dataWidthS || !dataHeightS) {
return undefined;
}
const childIdx = findIndexOfChild(pg[1].parentElement!, visitChain[0][1]);
if (childIdx < 0) {
return undefined;
}
(visitChain[0][1] as any) = childIdx;
const dataWidth = Number.parseFloat(dataWidthS);
const dataHeight = Number.parseFloat(dataHeightS);
const sourceNodePath = visitChain;
return sourceNodePath;
return {
page_no: Number.parseInt(pageNumber) + 1,
x: xPercent * dataWidth,
y: yPercent * dataHeight,
};
};
removeSourceMappingHandler(docRoot);
const sourceMappingHandler = ((docRoot as any).sourceMappingHandler = async (
event: MouseEvent
event: MouseEvent,
) => {
let elem = event.target! as Element;
const elementPath = await collectElementPath(event, elem);
if (!elementPath) {
const frameLoc = await resolveFrameLoc(event, elem);
if (!frameLoc) {
return;
}
console.log("element path", elementPath);
console.log("frameLoc", frameLoc);
window.typstWebsocket.send(`src-point ${JSON.stringify(frameLoc)}`);
const triggerWindow = document.body || document.firstElementChild;
const basePos = triggerWindow.getBoundingClientRect();
@ -338,22 +180,20 @@ export function installEditorJumpToHandler(docRoot: HTMLElement) {
left,
top,
"typst-debug-react-ripple",
"typst-debug-react-ripple-effect .4s linear"
"typst-debug-react-ripple-effect .4s linear",
);
window.typstWebsocket.send(`srcpath ${JSON.stringify(elementPath)}`);
return;
});
docRoot.addEventListener("click", sourceMappingHandler);
}
export interface TypstDebugJumpDocument {
}
export interface TypstDebugJumpDocument {}
export function provideDebugJumpDoc<
TBase extends GConstructor<TypstDocumentContext>
>(Base: TBase): TBase & GConstructor<TypstDebugJumpDocument> {
export function provideDebugJumpDoc<TBase extends GConstructor<TypstDocumentContext>>(
Base: TBase,
): TBase & GConstructor<TypstDebugJumpDocument> {
return class DebugJumpDocument extends Base {
constructor(...args: any[]) {
super(...args);

View file

@ -11,9 +11,7 @@ export interface TypstSvgDocument {
}
export function provideSvgDoc<
TBase extends GConstructor<
TypstDocumentContext & Partial<TypstCanvasDocument>
>,
TBase extends GConstructor<TypstDocumentContext & Partial<TypstCanvasDocument>>,
>(Base: TBase): TBase & GConstructor<TypstSvgDocument> {
return class SvgDocument extends Base {
/// canvas render ctoken
@ -55,9 +53,7 @@ export function provideSvgDoc<
}
const t2 = performance.now();
patchSvgToContainer(this.hookedElem, patchStr, (elem) =>
this.decorateSvgElement(elem, mode)
);
patchSvgToContainer(this.hookedElem, patchStr, (elem) => this.decorateSvgElement(elem, mode));
const t3 = performance.now();
if (this.cursorPaths) {
@ -101,10 +97,7 @@ export function provideSvgDoc<
const rectNextBase = foundUseNext?.getBBox();
const rect = {
// Some char does not have position so they are resolved to 0
right:
rectBase.width !== 0
? rectBase.x + rectBase.width
: rectNextBase?.x || 0,
right: rectBase.width !== 0 ? rectBase.x + rectBase.width : rectNextBase?.x || 0,
// todo: have bug
// top: textBase.height / 2,
};
@ -131,10 +124,7 @@ export function provideSvgDoc<
ry = Math.abs(ry);
// Creates a circle with 5px radius (but regard vertical and horizontal scale)
const t = document.createElementNS(
"http://www.w3.org/2000/svg",
"ellipse"
);
const t = document.createElementNS("http://www.w3.org/2000/svg", "ellipse");
t.classList.add("typst-svg-cursor");
t.setAttribute("cx", `${rect.right}`);
// t.setAttribute('cy', `${rect.top}`);
@ -201,16 +191,12 @@ export function provideSvgDoc<
let topEstimate = top - 1,
bottomEstimate = top + height + 1;
if (ch) {
const pages = Array.from(ch).filter((x) =>
x.classList.contains("typst-page")
);
const pages = Array.from(ch).filter((x) => x.classList.contains("typst-page"));
let minTop = 1e33,
maxBottom = -1e33,
accumulatedHeight = 0;
for (const page of pages) {
const pageHeight = Number.parseFloat(
page.getAttribute("data-page-height")!
);
const pageHeight = Number.parseFloat(page.getAttribute("data-page-height")!);
const translateY = Number.parseFloat(page.getAttribute("data-y")!);
if (translateY + pageHeight > topEstimate) {
minTop = Math.min(minTop, accumulatedHeight);
@ -236,7 +222,7 @@ export function provideSvgDoc<
topEstimate,
// hi.x, hi.y
left + width + 1,
bottomEstimate
bottomEstimate,
);
console.log(
"render_in_window with partial rendering enabled window",
@ -246,16 +232,10 @@ export function provideSvgDoc<
width,
height,
", patch scale",
patchStr.length
patchStr.length,
);
} else {
console.log(
"render_in_window with partial rendering disabled",
0,
0,
1e33,
1e33
);
console.log("render_in_window with partial rendering disabled", 0, 0, 1e33, 1e33);
patchStr = this.kModule.render_in_window(0, 0, 1e33, 1e33);
}
@ -333,8 +313,7 @@ export function provideSvgDoc<
private decorateSvgElement(svg: SVGElement, mode: PreviewMode) {
const container = this.cachedDOMState;
const kShouldMixinCanvas =
this.previewMode === PreviewMode.Doc && this.shouldMixinCanvas();
const kShouldMixinCanvas = this.previewMode === PreviewMode.Doc && this.shouldMixinCanvas();
// the <rect> could only have integer width and height
// so we scale it by 100 to make it more accurate
@ -354,7 +333,7 @@ export function provideSvgDoc<
const nextPages: SvgPage[] = (() => {
/// Retrieve original pages
const filteredNextPages = Array.from(svg.children).filter((x) =>
x.classList.contains("typst-page")
x.classList.contains("typst-page"),
);
if (mode === PreviewMode.Doc) {
@ -368,9 +347,7 @@ export function provideSvgDoc<
}
})().map((elem, index) => {
const width = Number.parseFloat(elem.getAttribute("data-page-width")!);
const height = Number.parseFloat(
elem.getAttribute("data-page-height")!
);
const height = Number.parseFloat(elem.getAttribute("data-page-height")!);
maxWidth = Math.max(maxWidth, width);
return {
index,
@ -420,7 +397,7 @@ export function provideSvgDoc<
inserter: (pageInfo) => {
const foreignObject = document.createElementNS(
"http://www.w3.org/2000/svg",
"foreignObject"
"foreignObject",
);
elem.appendChild(foreignObject);
foreignObject.setAttribute("width", `${width}`);
@ -436,11 +413,7 @@ export function provideSvgDoc<
for (let i = 0; i < nextPages.length; i++) {
/// Retrieve page width, height
const nextPage = nextPages[i];
const {
width: pageWidth,
height: pageHeight,
elem: pageElem,
} = nextPage;
const { width: pageWidth, height: pageHeight, elem: pageElem } = nextPage;
/// Switch a dummy svg page to canvas mode
if (kShouldMixinCanvas && isDummyPatchElem(pageElem)) {
@ -459,32 +432,19 @@ export function provideSvgDoc<
/// center the page and add margin
const calculatedPaddedX = (newWidth - pageWidth) / 2;
const calculatedPaddedY =
accumulatedHeight + (i == 0 ? 0 : heightMargin);
const calculatedPaddedY = accumulatedHeight + (i == 0 ? 0 : heightMargin);
const translateAttr = `translate(${calculatedPaddedX}, ${calculatedPaddedY})`;
/// Create inner rectangle
const innerRect = document.createElementNS(
"http://www.w3.org/2000/svg",
"rect"
);
const innerRect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
innerRect.setAttribute("class", "typst-page-inner");
innerRect.setAttribute("data-page-width", pageWidth.toString());
innerRect.setAttribute("data-page-height", pageHeight.toString());
innerRect.setAttribute(
"width",
Math.floor(pageWidth * INNER_RECT_UNIT).toString()
);
innerRect.setAttribute(
"height",
Math.floor(pageHeight * INNER_RECT_UNIT).toString()
);
innerRect.setAttribute("width", Math.floor(pageWidth * INNER_RECT_UNIT).toString());
innerRect.setAttribute("height", Math.floor(pageHeight * INNER_RECT_UNIT).toString());
innerRect.setAttribute("x", "0");
innerRect.setAttribute("y", "0");
innerRect.setAttribute(
"transform",
`${translateAttr} ${INNER_RECT_SCALE}`
);
innerRect.setAttribute("transform", `${translateAttr} ${INNER_RECT_SCALE}`);
if (this.pageColor) {
innerRect.setAttribute("fill", this.pageColor);
}
@ -497,6 +457,7 @@ export function provideSvgDoc<
pageElem.setAttribute("transform", translateAttr);
pageElem.setAttribute("data-x", `${calculatedPaddedX}`);
pageElem.setAttribute("data-y", `${calculatedPaddedY}`);
pageElem.setAttribute("data-page-number", `${nextPage.index}`);
/// Insert rectangles
// todo: this is buggy not preserving order?
@ -505,15 +466,9 @@ export function provideSvgDoc<
firstRect = innerRect;
}
const clipPath = document.createElementNS(
"http://www.w3.org/2000/svg",
"clipPath"
);
const clipPath = document.createElementNS("http://www.w3.org/2000/svg", "clipPath");
const clipRect = document.createElementNS(
"http://www.w3.org/2000/svg",
"rect"
);
const clipRect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
clipRect.setAttribute("x", "0");
clipRect.setAttribute("y", "0");
@ -526,8 +481,7 @@ export function provideSvgDoc<
clipPath.setAttribute("id", clipId);
pageElem.setAttribute("clip-path", `url(#${clipId})`);
let pageHeightEnd =
pageHeight + (i + 1 === nextPages.length ? 0 : heightMargin);
let pageHeightEnd = pageHeight + (i + 1 === nextPages.length ? 0 : heightMargin);
if (this.isContentPreview) {
// --typst-preview-toolbar-fg-color
@ -535,21 +489,14 @@ export function provideSvgDoc<
// console.log('create page number indicator', scale);
const pageNumberIndicator = document.createElementNS(
"http://www.w3.org/2000/svg",
"text"
);
pageNumberIndicator.setAttribute(
"class",
"typst-preview-svg-page-number"
"text",
);
pageNumberIndicator.setAttribute("class", "typst-preview-svg-page-number");
pageNumberIndicator.setAttribute("x", "0");
pageNumberIndicator.setAttribute("y", "0");
const onPaddedX = calculatedPaddedX + pageWidth / 2;
const onPaddedY =
calculatedPaddedY + pageHeight + heightMargin + fontSize / 2;
pageNumberIndicator.setAttribute(
"transform",
`translate(${onPaddedX}, ${onPaddedY})`
);
const onPaddedY = calculatedPaddedY + pageHeight + heightMargin + fontSize / 2;
pageNumberIndicator.setAttribute("transform", `translate(${onPaddedX}, ${onPaddedY})`);
pageNumberIndicator.setAttribute("font-size", fontSize.toString());
pageNumberIndicator.textContent = `${i + 1}`;
svg.append(pageNumberIndicator);
@ -558,18 +505,12 @@ export function provideSvgDoc<
} else {
if (this.cursorPosition && this.cursorPosition[0] === i + 1) {
const [_, x, y] = this.cursorPosition;
const cursor = document.createElementNS(
"http://www.w3.org/2000/svg",
"circle"
);
const cursor = document.createElementNS("http://www.w3.org/2000/svg", "circle");
cursor.setAttribute("cx", (x * INNER_RECT_UNIT).toString());
cursor.setAttribute("cy", (y * INNER_RECT_UNIT).toString());
cursor.setAttribute("r", (5 * scale * INNER_RECT_UNIT).toString());
cursor.setAttribute("fill", "#86C166CC");
cursor.setAttribute(
"transform",
`${translateAttr} ${INNER_RECT_SCALE}`
);
cursor.setAttribute("transform", `${translateAttr} ${INNER_RECT_SCALE}`);
svg.appendChild(cursor);
}
}
@ -593,9 +534,7 @@ export function provideSvgDoc<
if (ch?.tagName === "foreignObject") {
const canvasDiv = ch.firstElementChild as HTMLDivElement;
const pageNumber = Number.parseInt(
canvasDiv.getAttribute("data-page-number")!
);
const pageNumber = Number.parseInt(canvasDiv.getAttribute("data-page-number")!);
const pageInfo = n2CMapping.get(pageNumber);
if (pageInfo) {
pageInfo.container = canvasDiv as HTMLDivElement;
@ -616,7 +555,7 @@ export function provideSvgDoc<
console.assert(
this.canvasRenderCToken === undefined,
"No!!: canvasRenderCToken should be undefined"
"No!!: canvasRenderCToken should be undefined",
);
const tok = (this.canvasRenderCToken = new TypstCancellationToken());
@ -633,7 +572,7 @@ export function provideSvgDoc<
}
});
},
{ timeout: 1000 }
{ timeout: 1000 },
);
}
@ -648,10 +587,7 @@ export function provideSvgDoc<
if (firstPage) {
const rectHeight = Math.ceil(newHeight).toString();
const outerRect = document.createElementNS(
"http://www.w3.org/2000/svg",
"rect"
);
const outerRect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
outerRect.setAttribute("class", "typst-page-outer");
outerRect.setAttribute("data-page-width", newWidth.toString());
outerRect.setAttribute("data-page-height", rectHeight);
@ -680,7 +616,7 @@ export function provideSvgDoc<
if (svg) {
let svgWidth = Number.parseFloat(
svg.getAttribute("data-width")! || svg.getAttribute("width")! || "1"
svg.getAttribute("data-width")! || svg.getAttribute("width")! || "1",
);
if (svgWidth < 1e-5) {
svgWidth = 1;
@ -692,14 +628,11 @@ export function provideSvgDoc<
}
private statSvgFromDom() {
const { width: containerWidth, boundingRect: containerBRect } =
this.cachedDOMState;
const { width: containerWidth, boundingRect: containerBRect } = this.cachedDOMState;
// scale derived from svg width and container with.
// svg.setAttribute("data-width", `${newWidth}`);
const computedRevScale = containerWidth
? this.docWidth / containerWidth
: 1;
const computedRevScale = containerWidth ? this.docWidth / containerWidth : 1;
// respect current scale ratio
const revScale = computedRevScale / this.currentScaleRatio;
const left = (window.screenLeft - containerBRect.left) * revScale;