Move demo code into a single file
diff --git a/demo/index.js b/demo/index.js
index 78f0d23..c8fe1b5 100644
--- a/demo/index.js
+++ b/demo/index.js
@@ -19,15 +19,31 @@
   parse as parseFragment,
   stringify as stringifyFragment,
 } from '@annotator/fragment-identifier';
-import { describeTextQuoteByRange as describeRange } from '@annotator/dom';
-
-import { mark } from './mark.js';
-import { search } from './search.js';
+import {
+  createRangeSelectorCreator,
+  createTextQuoteSelector,
+  describeTextQuoteByRange as describeRange,
+} from '@annotator/dom';
+import { makeRefinable } from '@annotator/selector';
+import highlightRange from 'dom-highlight-range';
 
 function clear() {
   corpus.innerHTML = selectable.innerHTML;
 }
 
+const createSelector = makeRefinable(selector => {
+  const selectorCreator = {
+    TextQuoteSelector: createTextQuoteSelector,
+    RangeSelector: createRangeSelectorCreator(createSelector),
+  }[selector.type];
+
+  if (selectorCreator == null) {
+    throw new Error(`Unsupported selector type: ${selector.type}`);
+  }
+
+  return selectorCreator(selector);
+});
+
 const refresh = async () => {
   clear();
 
@@ -35,14 +51,15 @@
   if (!identifier) return;
 
   const { selector } = parseFragment(identifier);
+  const matchAll = createSelector(selector);
   const ranges = [];
 
-  for await (const range of search(corpus, selector)) {
+  for await (const range of matchAll(corpus)) {
     ranges.push(range);
   }
 
   for (const range of ranges) {
-    mark(range);
+    highlightRange(range, 'highlighted');
   }
 
   parsed.innerText = JSON.stringify(selector, null, 2);
diff --git a/demo/mark.js b/demo/mark.js
deleted file mode 100644
index 5ffbc2d..0000000
--- a/demo/mark.js
+++ /dev/null
@@ -1,25 +0,0 @@
-/**
- * @license
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
- */
-
-/**
- * Surround the contents of the given range with a mark tag.
- * @param {Range} range
- */
-
-import highlightRange from 'dom-highlight-range';
-
-export function mark(range) {
-  highlightRange(range, 'highlighted');
-}
diff --git a/demo/search.js b/demo/search.js
deleted file mode 100644
index e5145a3..0000000
--- a/demo/search.js
+++ /dev/null
@@ -1,43 +0,0 @@
-/**
- * @license
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
- */
-
-import { makeRefinable } from '@annotator/selector';
-import {
-  createRangeSelectorCreator,
-  createTextQuoteSelector,
-} from '@annotator/dom';
-
-const createSelector = makeRefinable(selector => {
-  const selectorCreator = {
-    TextQuoteSelector: createTextQuoteSelector,
-    RangeSelector: createRangeSelectorCreator(createSelector),
-  }[selector.type];
-
-  if (selectorCreator == null) {
-    throw new Error(`Unsupported selector type: ${selector.type}`);
-  }
-
-  return selectorCreator(selector);
-});
-
-/**
- * Locate a selector.
- * @param {Node} root node
- * @param {Selector} selector
- * @return {Range}
- */
-export async function* search(root, selector) {
-  yield* createSelector(selector)(root);
-}