From b9708aea55cccdd2fe12bd9719392064f02052c3 Mon Sep 17 00:00:00 2001 From: Tony Ross Date: Fri, 8 Mar 2019 14:55:14 -0600 Subject: [PATCH] New: Add API to get the location of content in an element Also update docs to explain how `report` can be used with both `element` and `location` to specify an offset into an element's content. --- .../docs/contributor-guide/how-to/hint.md | 3 +- packages/hint/src/lib/hint-context.ts | 16 +++-- packages/hint/src/lib/types/html.ts | 66 ++++++++++++++++--- 3 files changed, 72 insertions(+), 13 deletions(-) diff --git a/packages/hint/docs/contributor-guide/how-to/hint.md b/packages/hint/docs/contributor-guide/how-to/hint.md index 9c97e52886d..1e1197060c2 100644 --- a/packages/hint/docs/contributor-guide/how-to/hint.md +++ b/packages/hint/docs/contributor-guide/how-to/hint.md @@ -108,7 +108,8 @@ context.report(resource, message, { element: element }); * `content` is a string of text within `element` where the issue was found (used to refine a `ProblemLocation`).; * `location` is an explicit `ProblemLocation` (`{col: number, line: number}`) - where the issue was found. + where the issue was found. If used with `element`, it represents an offset + from the start of that element's content (e.g. for inline CSS in HTML). * `severity` overrides the default `Severity` for the hint to determine how the issue will be reported (e.g. `Severity.error`). diff --git a/packages/hint/src/lib/hint-context.ts b/packages/hint/src/lib/hint-context.ts index 40ae29263b3..0cf9c29f5d2 100644 --- a/packages/hint/src/lib/hint-context.ts +++ b/packages/hint/src/lib/hint-context.ts @@ -25,7 +25,10 @@ export type ReportOptions = { content?: string; /** The `HTMLElement` where the issue was found (used to get a `ProblemLocation`). */ element?: HTMLElement | null; - /** The `ProblemLocation` where the issue was found. */ + /** + * The `ProblemLocation` where the issue was found. + * If specified with `element`, represents an offset in the element's content (e.g. for inline CSS in HTML). + */ location?: ProblemLocation | null; /** The `Severity` to report the issue as (overrides default settings for a hint). */ severity?: Severity; @@ -102,18 +105,23 @@ export class HintContext { } /** Finds the approximative location in the page's HTML for a match in an element. */ - public findProblemLocation(element: HTMLElement): ProblemLocation | null { + public findProblemLocation(element: HTMLElement, offset: ProblemLocation | null): ProblemLocation | null { + if (offset) { + return element.getContentLocation(offset); + } + return element.getLocation(); } /** Reports a problem with the resource. */ public report(resource: string, message: string, options: ReportOptions = {}) { const { codeSnippet, element, severity } = options; - let position: ProblemLocation | null = options.location || null; + let position = options.location || null; let sourceCode: string | null = null; if (element) { - position = this.findProblemLocation(element); + // When element is provided, position is an offset in the content. + position = this.findProblemLocation(element, position); sourceCode = element.outerHTML().replace(/[\t]/g, ' '); } diff --git a/packages/hint/src/lib/types/html.ts b/packages/hint/src/lib/types/html.ts index 35008abb038..668e7642dff 100644 --- a/packages/hint/src/lib/types/html.ts +++ b/packages/hint/src/lib/types/html.ts @@ -79,30 +79,80 @@ export class HTMLElement { } /** - * Zero-based location of the element. + * Helper to find the original location in source of an element. + * Used when this element is part of a DOM snapshot to search the + * original fetched document a similar element and use the location + * of that element instead. */ - public getLocation(): ProblemLocation | null { + private _getOriginalLocation(): parse5.ElementLocation | null { const location = this._element.sourceCodeLocation; // Use direct location information when available. if (location) { - return { - // Column is zero-based, but pointing to the tag name, not the character < - column: location.startCol, - line: location.startLine - 1 - }; + return location; } // If not, try to match an element in the original document to use it's location. if (this.ownerDocument && this.ownerDocument.originalDocument) { const match = findOriginalElement(this.ownerDocument.originalDocument, this); - return match ? match.getLocation() : null; + if (match) { + return match._element.sourceCodeLocation; + } } + // Otherwise we don't have a location (element may have been dynamically generated). return null; } + /** + * Zero-based location of the element. + */ + public getLocation(): ProblemLocation | null { + const location = this._getOriginalLocation(); + + if (!location) { + return null; + } + + // Column is zero-based, but pointing to the tag name, not the character < + return { + column: location.startCol, + line: location.startLine - 1 + }; + } + + /** + * Calculate the document location of content within this element. + * Used to determine offsets for CSS-in-HTML and JS-in-HTML reports. + */ + public getContentLocation(offset: ProblemLocation): ProblemLocation | null { + const location = this._getOriginalLocation(); + + if (!location) { + return null; + } + + // Get the end of the start tag from `parse5`, converting to be zero-based. + const startTag = location.startTag; + const column = startTag.endCol - 1; + const line = startTag.endLine - 1; + + // Adjust resulting column when content is on the same line as the tag. + if (offset.line === 0) { + return { + column: column + offset.column, + line + }; + } + + // Otherwise adjust just the resulting line. + return { + column: offset.column, + line: line + offset.line + }; + } + public isSame(element: HTMLElement): boolean { return this._element === element._element; }