Browse Source

Initial implementation, polyfill, demo

Based on the Text Fragments draft spec version of 12 Aug 2020.
tags/v0.1.0-src
Gerben 9 months ago
parent
commit
67ed705b1a
8 changed files with 1697 additions and 0 deletions
  1. +2
    -0
      .gitignore
  2. +12
    -0
      Readme.md
  3. +143
    -0
      demo.html
  4. +14
    -0
      package-lock.json
  5. +27
    -0
      package.json
  6. +1396
    -0
      src/index.ts
  7. +82
    -0
      src/polyfill.ts
  8. +21
    -0
      tsconfig.json

+ 2
- 0
.gitignore View File

@@ -0,0 +1,2 @@
/node_modules
/lib

+ 12
- 0
Readme.md View File

@@ -0,0 +1,12 @@
# Text Fragments — TypeScript implementation

This is an implementation of (most of) the [WICG’s Text Fragments draft specification: <https://wicg.github.io/scroll-to-text-fragment/>][spec].

It implements the spec line by line, in order to help serve as a reference implementation and to help test the specification. No attempt is made to e.g. increase efficiency.

A polyfill is provided to use text fragments in browsers and other HTML viewers that do not support this feature natively.

Try it out in the [playground][]!

[spec]: https://wicg.github.io/scroll-to-text-fragment/
[playground]: https://temp.treora.com/text-fragments-ts/demo.html

+ 143
- 0
demo.html View File

@@ -0,0 +1,143 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>Text Fragments playground</title>
<script src="./lib/polyfill.js" type="module"></script>
<style>
body {
margin: 0;
line-height: 1.8;
font-family: sans-serif;
}
main {
max-width: 30em;
margin: auto;
font-family: serif;
}
header {
background-color: linen;
padding: 1em;
}
header div {
max-width: 48em;
margin: auto;
}
aside {
background-color:floralwhite;
padding: 1em;
}
h2 {
font-size: 1em;
}
a {
text-decoration: none;
}
</style>
</head>
<body>
<header>
<div>
<h1>Text Fragments playground</h1>
<p>
This page demonstrates <a href="https://wicg.github.io/scroll-to-text-fragment/">Text Fragments</a> using the <a href="https://code.treora.com/gerben/text-fragments-ts">text-fragments-ts</a> implementation as a polyfill</a>.
</p>
<p style="background: whitesmoke; padding: 1em;">
Is this page currently using the polyfill?
<b id="usingPolyfill">
<em>Detecting failed (or page is loading?)</em>
<script>
window.addEventListener('load', () => {
const fragmentDirective = window.location.fragmentDirective;
const message = (fragmentDirective
? fragmentDirective._implementation === 'text-fragments-ts'
? 'Yes'
: 'No <em>(your browser appears to support text fragments already!)</em>'
: 'No <em>(something went wrong?)</em>'
);
usingPolyfill.innerHTML = message;
});
</script>
<noscript>No <em>(Javascript is disabled)</em></noscript>
</b>
</p>
<p>
Try click some of the links below: they point at specific quotes within this text, which the browser should then scroll to and highlight.
</p>
<ul>
<li>
<a href="#:~:text=semper ut commodo">Simple quote</a>
</li>
<li>
<a href="#:~:text=placerat.%20Vivamus">Text spanning multiple elements</a>
</li>
<li>
<a href="#:~:text=Cras%20et-,ipsum">Using prefix (pointing a specific occurrence of ‘ipsum’ by providing words before it)</a>
</li>
<li>
<a href="#:~:text=ipsum,-sollicitudin">Using suffix (for the same ‘ipsum’)</a>
</li>
<li>
<a href="#:~:text=sit-,amet,-auctor">Both prefix and suffix</a>
</li>
<li>
<a href="#:~:text=word?-,Not%20sure.">Text making up a whole, reoccurring block element</a>
</li>
<li>
<a href="#:~:text=Fusce%20quis,est.">Select text between two given phrases</a>
</li>
<li>
<a href="#:~:text=Aliquam%20urna,scelerisque.">Text between two phrases in different block elements</a>
</li>
<li>
<a href="#:~:text=GATTACA,CATATTAC">Select text between start and end of a long, uninterrupted string <em>(fails)</em></a>
</li>
<li>
<a href="#:~:text=poi-,i,-nt">Point at letters (e.g. a typo) within a word <em>(fails)</em></a>
</li>
</ul>
<p>
You can also try write your own target quotes in the URL bar.
</p>
</div>
</header>
<!-- <aside>
<h2>Options</h2>
<input type="checkbox" id="checkboxWide" onchange="main.style.whiteSpace = this.checked ? 'nowrap' : null"> <label for="checkboxWide">Horizonal layout (to test scrolling)</label>
<script>checkboxWide.checked = false; // To ensure consistency when page is refreshed.</script>
</aside> -->
<main id="main">
<h1>Bla bla bla</h1>
<p>
Lorem ipsum dolor sit amet, <i>consectetur adipiscing elit</i>. Morbi ligula magna, semper a quam sit amet, malesuada tristique dui. Ut vitae diam massa. Proin gravida neque nec libero suscipit placerat. <b>Vivamus viverra <i>ligula vitae</i> orci fringilla vulputate</b>. Vivamus venenatis leo at venenatis venenatis. Mauris quam sem, sagittis sit amet mi quis, placerat laoreet eros. Suspendisse porta neque sit amet bibendum condimentum. Proin <a href="#:~:text=efficitur%20nulla%20aliquam">posuere purus</a> tellus, ac condimentum justo sollicitudin rhoncus. Integer nisl elit, convallis a velit nec, tristique convallis enim. Pellentesque placerat et purus <a href="#:~:text=semper%20ut%20commodo">porttitor tincidunt</a>.
</p>
<p>
Phasellus tempus dui vitae velit efficitur, nec volutpat lorem imperdiet. Aliquam hendrerit lectus at erat molestie, a porta ipsum interdum. Donec justo ex, porta sit amet ipsum eget, commodo ultrices ex. Integer dapibus euismod ante non ultrices. Donec commodo magna id turpis condimentum convallis. Quisque tincidunt quam vitae fringilla mattis. Aenean mattis commodo dolor ac imperdiet. Maecenas libero est, placerat porttitor libero quis, mollis condimentum nunc. Praesent in quam vel velit gravida cursus. Cras facilisis lectus in lectus pellentesque, at imperdiet odio elementum. Suspendisse pulvinar dui et ligula sagittis, nec tristique tellus congue. Aenean sed nulla in quam malesuada elementum in nec nulla. Aliquam sed cursus metus. Cras a molestie augue, id sodales velit. Proin blandit justo sed ante placerat consectetur. Aliquam urna purus, tempor eu cursus at, pharetra ac velit.
</p>
<p>
Proin accumsan mollis scelerisque. Sed id magna consectetur, tincidunt ipsum nec, semper velit. Morbi aliquam quis nisi non volutpat. Pellentesque a ultrices ante. In congue volutpat odio, vitae efficitur nulla aliquam quis. Vivamus cursus venenatis efficitur. Sed at ornare purus. Maecenas est justo, tincidunt ac convallis at, accumsan nec ipsum. Vestibulum tortor ligula, vestibulum a nisl sit amet, lobortis consectetur eros. Sed augue dui, porta at justo sit amet, venenatis convallis sapien. Praesent vel malesuada tortor, at iaculis diam.
</p>
<p>
Integer egestas, justo at vestibulum consequat, metus turpis aliquam felis, a tincidunt dui lorem eget orci. Fusce quis feugiat sapien, quis pharetra nulla. Sed suscipit mauris non tincidunt suscipit. Vestibulum nec mollis est. Donec eget interdum urna. Cras placerat nulla nec orci pretium blandit. Phasellus eget odio imperdiet, interdum tortor vel, convallis ante. Pellentesque tristique convallis ultrices. Cras et ipsum sollicitudin, elementum nulla tempus, tempor tellus. In hac habitasse platea dictumst. Nam in aliquam neque.
</p>
<p>
Vestibulum eu tristique elit. Sed ac ipsum sed sapien ultricies dapibus. Cras efficitur aliquet luctus. Aliquam sit amet auctor tellus, nec rhoncus nisl. Integer at lobortis sapien. Nunc mattis tristique libero, sed ultrices nunc imperdiet eu. Pellentesque accumsan, eros non auctor eleifend, felis massa bibendum lacus, non venenatis orci sapien eu sapien. Integer eu eros fringilla lectus vestibulum aliquam. Cras consectetur nunc nisi, vel molestie justo congue at. Duis eros neque, semper ut commodo in, molestie ut nunc. Vivamus vitae bibendum magna, suscipit sollicitudin elit. Praesent id rhoncus enim, sit amet suscipit velit. Aenean euismod purus velit, et consectetur nulla gravida vitae.
</p>
<p>
The Chrome-osome is GATTACAGACTGCGATACGTTACTAGTTAGGACTACGGGATCATATTAC. Can we select it without quoting the whole thing?
</p>
<p>
Not sure.
</p>
<p>
And how to poiint at letters (e.g. a typo) within a word?
</p>
<p>
Not sure.
</p>
<p>
Finally, <a href="#:~:text=Text%20Fragments%20playground">a link to scroll back up.</a>
</p>
</main>
</body>
</html>

+ 14
- 0
package-lock.json View File

@@ -0,0 +1,14 @@
{
"name": "text-fragments-ts",
"version": "0.1.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"typescript": {
"version": "3.9.7",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.7.tgz",
"integrity": "sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==",
"dev": true
}
}
}

+ 27
- 0
package.json View File

@@ -0,0 +1,27 @@
{
"name": "text-fragments-ts",
"version": "0.1.0",
"description": "The WICG’s Text Fragments draft specification, implemented in TypeScript",
"author": "Gerben",
"homepage": "https://code.treora.com/gerben/text-fragments-ts",
"repository": {
"type": "git",
"url": "https://code.treora.com/gerben/text-fragments-ts"
},
"scripts": {
"build": "tsc"
},
"type": "module",
"exports": {
".": "./lib/index.js",
"./polyfill": "./lib/polyfill.js"
},
"module": "lib/index.js",
"files": [
"lib"
],
"dependencies": {},
"devDependencies": {
"typescript": "^3.9.7"
}
}

+ 1396
- 0
src/index.ts
File diff suppressed because it is too large
View File


+ 82
- 0
src/polyfill.ts View File

@@ -0,0 +1,82 @@
// A polyfill that makes the browser scroll to, and highlight, the Text Fragment given in the location’s fragment directive.
// See https://wicg.github.io/scroll-to-text-fragment/
// Based on the version of 12 August 2020. <https://raw.githubusercontent.com/WICG/scroll-to-text-fragment/60f5f63b4997bde7e688cacf897e1167c622e100/index.html>

// This implementation assumes the browser has already performed the normal procedures to identify and scroll to the fragment, without support for Text Fragments.

import {
initializeDocumentFragmentDirective,
indicatedPartOfTheDocument_beginning,
scrollToTheFragment,
FragmentDirective,
browserSupportsTextFragments,
} from './index.js';

function run(): void {
const { documentUrl, documentFragmentDirective } = initializeDocumentFragmentDirective(window.document) ?? {};
if (documentUrl !== document.URL) {
// We could change the location to hide the fragment directive from the fragment, as the spec prescribes; however this would also hide it from the user (and could trigger other event listeners).
// document.location.replace(documentUrl);
}

if (documentFragmentDirective !== null) {
const { documentIndicatedPart, ranges } = indicatedPartOfTheDocument_beginning({
document,
documentFragmentDirective,
documentAllowTextFragmentDirective: true, // TEMP (TODO should be determined if possible)
}) || undefined;

if (documentIndicatedPart !== undefined) {
scrollToTheFragment(documentIndicatedPart);
}

if (ranges !== undefined) {
highlightRanges(ranges);
}
}
}

function pretendBrowserSupportsTextFragments(): void {
const fragmentDirective: FragmentDirective = {};

// Sneak in a note so one can discover whether the polyfill is used.
Object.defineProperty(fragmentDirective, '_implementation', {
value: 'text-fragments-ts',
enumerable: false,
});

Object.defineProperty(window.location, 'fragmentDirective', {
value: fragmentDirective,
writable: false,
});
}

// See § 3.6. Indicating The Text Match <https://wicg.github.io/scroll-to-text-fragment/#indicating-the-text-match>
// This implements a simple method to highlight the indicated ranges, without modifying the DOM: we use the window’s selection. This has the limitation that it disappears as soon as the user clicks anywhere; but the ability to dismiss it is a feature too; and it helps convey that the highlight is not part of the page itself.
// Note the spec urges against this approach: “the UA must not use the Document’s selection to indicate the text match as doing so could allow attack vectors for content exfiltration.”
// XXX How exactly could this be an attack vector?
function highlightRanges(ranges: Range[]): void {
const selection = window.getSelection() as Selection; // should be non-null on top window.
selection.removeAllRanges();
for (const range of ranges) {
selection.addRange(range);
}
}

function install(): void {
// Do nothing if the browser already supports (text) fragment directives.
if (browserSupportsTextFragments())
return;

pretendBrowserSupportsTextFragments();

// Run when the page is ready.
window.addEventListener('load', run);
// Could we somehow avoid activating in cases where the browser would retain scroll position, e.g. on page reload or history navigation?

// Run whenever the location’s fragment identifier is changed.
window.addEventListener('hashchange', run);
// Could we somehow also detect it when the user navigates to exactly the same fragment again? (to mimic browser/Firefox’s behaviour when just pressing enter in the URL bar)
}

install();

+ 21
- 0
tsconfig.json View File

@@ -0,0 +1,21 @@
{
"compilerOptions": {
"declaration": true,
"declarationMap": true,
"lib": [
"DOM",
"DOM.Iterable",
"ES2020"
],
"moduleResolution": "node",
"module": "ES2015",
"outDir": "lib",
"rootDir": "src",
"sourceMap": true,
"strict": true,
"target": "ES2017"
},
"include": [
"src"
],
}

Loading…
Cancel
Save