openbriefing 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  This package is the reference parser and checker for the [Open Briefing Standard (OBS)](https://openbriefingstandard.org). OBS is how a publisher says how their content should be summarized and presented when software consumes it.
4
4
 
5
- **Version 0.1.** It contains the parser and `openbriefing check`. Later versions add `generate`, `preview`, `backfill`, `review` and a Ghost integration.
5
+ **Version 0.2.** It contains the parser and `openbriefing check`, and reads spec version 1.1, which adds `obs:highlight`: passages the publisher marks, word for word from the content, for quoting on their own with credit. `check` lists each highlight with its word count and label, and warns when one is not found in the page's text. Later versions add `generate`, `preview`, `backfill`, `review` and a Ghost integration.
6
6
 
7
7
  ## Check a page or a feed
8
8
 
@@ -22,6 +22,7 @@ import { parseObs } from "openbriefing";
22
22
 
23
23
  const dom = new JSDOM(html, { url });
24
24
  const { brief, errors, warnings } = parseObs(dom.window.document, url);
25
+ // brief.summaries, brief.keep, brief.highlights (each { text, words, label?, inContent? }), …
25
26
  ```
26
27
 
27
28
  | Export | What it does |
@@ -30,7 +31,7 @@ const { brief, errors, warnings } = parseObs(dom.window.document, url);
30
31
  | `parseFeedObs(itemElement, itemUrl?)` | Reads one RSS `<item>` or Atom `<entry>` |
31
32
  | `validateObs(object)` | Checks an object already in the `ObsBrief` shape |
32
33
  | `mergeObs(page, feed)` | Applies the feed-against-page rule: the later `obs:modified` stands, and the page wins a tie |
33
- | `tierFor(words)`, `SIZES`, `normalizeUrl` | Helpers that use the spec's numbers |
34
+ | `tierFor(words)`, `SIZES`, `normalizeUrl`, `looseText` | Helpers that use the spec's numbers and its loose text comparison |
34
35
 
35
36
  The parser has no runtime dependencies. It takes any DOM `Document` (jsdom, linkedom, or a browser's `DOMParser`), makes no network requests, and never throws.
36
37
 
package/dist/cli.js CHANGED
@@ -61,6 +61,14 @@ function describe(label, r) {
61
61
  row(`summary`, `${s.tier.padEnd(6)} ${String(s.words).padStart(5)} words (target ${lo}–${hi}) ${s.from}`);
62
62
  }
63
63
  b.keep.forEach((k, i) => row(i === 0 ? "keep (core)" : `keep ${i + 1}`, cut(k)));
64
+ b.highlights.forEach((h, i) => {
65
+ const notes = [`${h.words}/${SIZES.highlight} words`];
66
+ if (h.label !== undefined)
67
+ notes.push(cut(h.label, 30));
68
+ if (h.inContent === false)
69
+ notes.push("not in the text");
70
+ row(`highlight ${i + 1}`, `${cut(h.text)} (${notes.join(", ")})`);
71
+ });
64
72
  row("derive", `${b.derive.mode}; translate ${b.derive.translate ? "yes" : "no"}; combine ${b.derive.combine ? "yes" : "no"}`);
65
73
  if (b.attribution !== undefined)
66
74
  row("attribution", cut(b.attribution));
package/dist/obs.d.ts CHANGED
@@ -18,6 +18,15 @@ export interface ObsSummary {
18
18
  /** "tiered" for obs:summary:{tier}; "inferred" for a bare obs:summary (§4). */
19
19
  from: "tiered" | "inferred";
20
20
  }
21
+ export interface ObsHighlight {
22
+ /** The passage, as written. */
23
+ text: string;
24
+ words: number;
25
+ /** obs:highlight:label, 6 words or fewer, when given. */
26
+ label?: string;
27
+ /** False when the page's text does not contain the highlight (§7). Absent for feed items and empty pages. */
28
+ inContent?: boolean;
29
+ }
21
30
  export interface ObsBrief {
22
31
  version: number;
23
32
  /** obs:modified, as given (ISO 8601). */
@@ -32,6 +41,8 @@ export interface ObsBrief {
32
41
  summaries: ObsSummary[];
33
42
  /** Priority order, at most 3. keep[0] is the core message (§6). */
34
43
  keep: string[];
44
+ /** Priority order, at most 5 (§7). Empty when none. */
45
+ highlights: ObsHighlight[];
35
46
  derive: {
36
47
  mode: DeriveMode;
37
48
  translate: boolean;
@@ -56,6 +67,9 @@ export declare const SIZES: {
56
67
  readonly keep: 40;
57
68
  readonly attribution: 12;
58
69
  readonly keepMax: 3;
70
+ readonly highlight: 60;
71
+ readonly highlightLabel: 6;
72
+ readonly highlightMax: 5;
59
73
  readonly tiers: Record<ObsTier, {
60
74
  target: [number, number];
61
75
  }>;
@@ -68,8 +82,13 @@ export declare const SIZES: {
68
82
  export declare function tierFor(words: number): ObsTier;
69
83
  /** Words are runs of characters between white space (§4). */
70
84
  export declare function countWords(text: string): number;
71
- /** URL comparison of §11: scheme and host case, trailing slash, tracking parameters. */
85
+ /** URL comparison of §12: scheme and host case, trailing slash, tracking parameters. */
72
86
  export declare function normalizeUrl(input: string, base?: string): string | undefined;
87
+ /**
88
+ * The loose comparison of §7: compatibility forms, straight quotes for curly,
89
+ * one kind of dash, one kind of space, one case. Words themselves must match.
90
+ */
91
+ export declare function looseText(s: string): string;
73
92
  /**
74
93
  * Reads a page's obs: meta tags. `pageUrl` is where the page was fetched
75
94
  * from; the page's <link rel="canonical"> takes precedence for §11.
package/dist/obs.js CHANGED
@@ -15,6 +15,9 @@ export const SIZES = {
15
15
  keep: 40,
16
16
  attribution: 12,
17
17
  keepMax: 3,
18
+ highlight: 60,
19
+ highlightLabel: 6,
20
+ highlightMax: 5,
18
21
  tiers: {
19
22
  short: { target: [40, 80] },
20
23
  medium: { target: [200, 350] },
@@ -36,6 +39,7 @@ export function countWords(text) {
36
39
  const t = text.trim();
37
40
  return t === "" ? 0 : t.split(/\s+/u).length;
38
41
  }
42
+ const newBag = () => ({ props: new Map(), highlights: [] });
39
43
  const KNOWN = new Set([
40
44
  "obs:modified",
41
45
  "obs:summary",
@@ -45,6 +49,8 @@ const KNOWN = new Set([
45
49
  "obs:title",
46
50
  "obs:headline",
47
51
  "obs:keep",
52
+ "obs:highlight",
53
+ "obs:highlight:label",
48
54
  "obs:derive",
49
55
  "obs:derive:translate",
50
56
  "obs:derive:combine",
@@ -55,14 +61,33 @@ const KNOWN = new Set([
55
61
  "obs:version",
56
62
  ]);
57
63
  function put(bag, key, value) {
58
- const list = bag.get(key);
64
+ const list = bag.props.get(key);
59
65
  if (list)
60
66
  list.push(value);
61
67
  else
62
- bag.set(key, [value]);
68
+ bag.props.set(key, [value]);
63
69
  }
70
+ /** Files one property, routing highlights and their labels to the ordered list (§3.1). */
71
+ function collect(bag, key, value, warnings) {
72
+ if (key === "obs:highlight") {
73
+ bag.highlights.push({ text: value });
74
+ }
75
+ else if (key === "obs:highlight:label") {
76
+ const last = bag.highlights[bag.highlights.length - 1];
77
+ if (last === undefined)
78
+ warnings.push("obs:highlight:label before any obs:highlight; it is ignored (§3.1)");
79
+ else if (last.label === undefined)
80
+ last.label = value;
81
+ else
82
+ warnings.push(`obs:highlight #${bag.highlights.length} has two labels; the first is used (§3.1)`);
83
+ }
84
+ else {
85
+ put(bag, key, value);
86
+ }
87
+ }
88
+ const isEmpty = (bag) => bag.props.size === 0 && bag.highlights.length === 0;
64
89
  function first(bag, key, warnings) {
65
- const list = bag.get(key);
90
+ const list = bag.props.get(key);
66
91
  if (!list || list.length === 0)
67
92
  return undefined;
68
93
  if (list.length > 1 && key !== "obs:keep") {
@@ -77,14 +102,14 @@ function cleanText(key, raw, warnings) {
77
102
  let text = raw;
78
103
  if (TAG_RE.test(text)) {
79
104
  text = text.replace(TAG_RE, "");
80
- warnings.push(`${key}: HTML removed; values are plain text (§8)`);
105
+ warnings.push(`${key}: HTML removed; values are plain text (§9)`);
81
106
  }
82
107
  TAG_RE.lastIndex = 0;
83
108
  text = text.replace(/\r\n?/gu, "\n").trim();
84
109
  if (text === "")
85
110
  return undefined;
86
111
  if (URL_RE.test(text))
87
- warnings.push(`${key}: contains a URL; summaries should not (§8)`);
112
+ warnings.push(`${key}: contains a URL; summaries should not (§9)`);
88
113
  return text;
89
114
  }
90
115
  function enumValue(key, raw, allowed, fallback, warnings) {
@@ -102,7 +127,7 @@ function validModified(v) {
102
127
  return ISO_RE.test(v) && !Number.isNaN(Date.parse(v));
103
128
  }
104
129
  const TRACKING = /^(?:utm_.*|fbclid|gclid)$/iu;
105
- /** URL comparison of §11: scheme and host case, trailing slash, tracking parameters. */
130
+ /** URL comparison of §12: scheme and host case, trailing slash, tracking parameters. */
106
131
  export function normalizeUrl(input, base) {
107
132
  let u;
108
133
  try {
@@ -130,11 +155,13 @@ function sizeWarning(key, words, max, warnings) {
130
155
  function build(bag, ctx, extraWarnings = []) {
131
156
  const errors = [];
132
157
  const warnings = [...extraWarnings];
133
- for (const key of bag.keys()) {
158
+ for (const key of bag.props.keys()) {
134
159
  if (KNOWN.has(key))
135
160
  continue;
136
161
  if (key.startsWith("obs:audio:") || key === "obs:audio")
137
- continue; // reserved, §14
162
+ continue; // reserved, §15
163
+ if (key === "obs:highlight:start" || key === "obs:highlight:end")
164
+ continue; // reserved, §15
138
165
  warnings.push(`${key} is not an OBS v1 property and is ignored (§3.1)`);
139
166
  }
140
167
  // Text values, cleaned once.
@@ -199,7 +226,7 @@ function build(bag, ctx, extraWarnings = []) {
199
226
  if (headline !== undefined)
200
227
  sizeWarning("obs:headline", countWords(headline), SIZES.headline, warnings);
201
228
  // Keep lines (§6)
202
- const keepAll = (bag.get("obs:keep") ?? [])
229
+ const keepAll = (bag.props.get("obs:keep") ?? [])
203
230
  .map((raw) => cleanText("obs:keep", raw, warnings))
204
231
  .filter((v) => v !== undefined);
205
232
  if (keepAll.length > SIZES.keepMax) {
@@ -207,6 +234,31 @@ function build(bag, ctx, extraWarnings = []) {
207
234
  }
208
235
  const keep = keepAll.slice(0, SIZES.keepMax);
209
236
  keep.forEach((k, i) => sizeWarning(`obs:keep #${i + 1}`, countWords(k), SIZES.keep, warnings));
237
+ // Highlights (§7)
238
+ const highlightsAll = [];
239
+ for (const raw of bag.highlights) {
240
+ const t = cleanText("obs:highlight", raw.text, warnings);
241
+ if (t === undefined)
242
+ continue;
243
+ const h = { text: t, words: countWords(t) };
244
+ if (raw.label !== undefined) {
245
+ const label = cleanText("obs:highlight:label", raw.label, warnings);
246
+ if (label !== undefined)
247
+ h.label = label;
248
+ }
249
+ if (typeof raw.inContent === "boolean")
250
+ h.inContent = raw.inContent;
251
+ highlightsAll.push(h);
252
+ }
253
+ if (highlightsAll.length > SIZES.highlightMax) {
254
+ warnings.push(`obs:highlight appears ${highlightsAll.length} times; only the first ${SIZES.highlightMax} are used (§3)`);
255
+ }
256
+ const highlights = highlightsAll.slice(0, SIZES.highlightMax);
257
+ highlights.forEach((h, i) => {
258
+ sizeWarning(`obs:highlight #${i + 1}`, h.words, SIZES.highlight, warnings);
259
+ if (h.label !== undefined)
260
+ sizeWarning(`obs:highlight:label #${i + 1}`, countWords(h.label), SIZES.highlightLabel, warnings);
261
+ });
210
262
  // Derivation (§5)
211
263
  const mode = enumValue("obs:derive", first(bag, "obs:derive", warnings), ["verbatim", "anchor", "open"], "anchor", warnings);
212
264
  const translate = enumValue("obs:derive:translate", first(bag, "obs:derive:translate", warnings), ["yes", "no"], "yes", warnings) === "yes";
@@ -216,7 +268,7 @@ function build(bag, ctx, extraWarnings = []) {
216
268
  sizeWarning("obs:attribution", countWords(attribution), SIZES.attribution, warnings);
217
269
  const preference = enumValue("obs:preference", first(bag, "obs:preference", warnings), ["summary", "full"], "summary", warnings);
218
270
  const lang = first(bag, "obs:lang", warnings)?.trim() || ctx.lang || undefined;
219
- // Identity (§11)
271
+ // Identity (§12)
220
272
  let url;
221
273
  let describesThisPage = true;
222
274
  const rawUrl = first(bag, "obs:url", warnings)?.trim();
@@ -235,6 +287,7 @@ function build(bag, ctx, extraWarnings = []) {
235
287
  describesThisPage,
236
288
  summaries,
237
289
  keep,
290
+ highlights,
238
291
  derive: { mode, translate, combine },
239
292
  preference,
240
293
  };
@@ -250,6 +303,34 @@ function build(bag, ctx, extraWarnings = []) {
250
303
  brief.lang = lang;
251
304
  return { brief, errors, warnings };
252
305
  }
306
+ /**
307
+ * The loose comparison of §7: compatibility forms, straight quotes for curly,
308
+ * one kind of dash, one kind of space, one case. Words themselves must match.
309
+ */
310
+ export function looseText(s) {
311
+ return s
312
+ .normalize("NFKC")
313
+ .replace(/[\u2018\u2019\u201a\u201b\u2032]/gu, "'")
314
+ .replace(/[\u201c\u201d\u201e\u201f\u2033]/gu, '"')
315
+ .replace(/[\u2010-\u2015\u2212]/gu, "-")
316
+ .replace(/\s*-\s*/gu, "-")
317
+ .replace(/\s+/gu, " ")
318
+ .trim()
319
+ .toLowerCase();
320
+ }
321
+ /** Sets inContent on each highlight from the page's text; skipped when the body is empty (§7). */
322
+ function checkContent(brief, bodyText, warnings) {
323
+ const body = bodyText === undefined ? "" : looseText(bodyText);
324
+ if (body === "")
325
+ return;
326
+ for (const h of brief.highlights) {
327
+ h.inContent = body.includes(looseText(h.text));
328
+ if (!h.inContent) {
329
+ const head = h.text.split(/\s+/u).slice(0, 6).join(" ");
330
+ warnings.push(`obs:highlight "${head}…" is not in the page's text (§7)`);
331
+ }
332
+ }
333
+ }
253
334
  function guard(fn) {
254
335
  try {
255
336
  return fn();
@@ -266,7 +347,7 @@ function guard(fn) {
266
347
  */
267
348
  export function parseObs(doc, pageUrl) {
268
349
  return guard(() => {
269
- const bag = new Map();
350
+ const bag = newBag();
270
351
  const warnings = [];
271
352
  let inBody = false;
272
353
  for (const meta of Array.from(doc.getElementsByTagName("meta"))) {
@@ -280,9 +361,9 @@ export function parseObs(doc, pageUrl) {
280
361
  continue;
281
362
  if (!inBody && doc.body && doc.body.contains(meta))
282
363
  inBody = true;
283
- put(bag, key, content);
364
+ collect(bag, key, content, warnings);
284
365
  }
285
- if (bag.size === 0)
366
+ if (isEmpty(bag))
286
367
  return { errors: ["No obs: tags on the page"], warnings };
287
368
  if (inBody)
288
369
  warnings.push("Some obs: tags are in <body>; they belong in <head> (§3.1)");
@@ -294,11 +375,14 @@ export function parseObs(doc, pageUrl) {
294
375
  ctx.canonical = canonical;
295
376
  if (lang !== undefined)
296
377
  ctx.lang = lang;
297
- return build(bag, ctx, warnings);
378
+ const result = build(bag, ctx, warnings);
379
+ if (result.brief)
380
+ checkContent(result.brief, doc.body?.textContent ?? undefined, result.warnings);
381
+ return result;
298
382
  });
299
383
  }
300
384
  // ---------------------------------------------------------------------------
301
- // Feeds (§13)
385
+ // Feeds (§14)
302
386
  function obsChildren(item) {
303
387
  return Array.from(item.children).filter((el) => {
304
388
  if (el.namespaceURI === OBS_NS)
@@ -335,12 +419,18 @@ function itemLink(item) {
335
419
  */
336
420
  export function parseFeedObs(item, itemUrl) {
337
421
  return guard(() => {
338
- const bag = new Map();
422
+ const bag = newBag();
339
423
  const warnings = [];
340
424
  for (const el of obsChildren(item)) {
341
425
  const name = localName(el);
342
426
  const value = el.textContent ?? "";
343
- if (name === "summary") {
427
+ if (name === "highlight") {
428
+ collect(bag, "obs:highlight", value, warnings);
429
+ const label = el.getAttribute("label");
430
+ if (label !== null)
431
+ collect(bag, "obs:highlight:label", label, warnings);
432
+ }
433
+ else if (name === "summary") {
344
434
  const length = el.getAttribute("length")?.trim().toLowerCase();
345
435
  if (length === undefined || length === "")
346
436
  put(bag, "obs:summary", value);
@@ -363,7 +453,7 @@ export function parseFeedObs(item, itemUrl) {
363
453
  put(bag, `obs:${name}`, value);
364
454
  }
365
455
  }
366
- if (bag.size === 0)
456
+ if (isEmpty(bag))
367
457
  return { errors: ["No OBS elements on the item"], warnings };
368
458
  const link = itemUrl ?? itemLink(item);
369
459
  const canonical = link ? normalizeUrl(link) : undefined;
@@ -382,7 +472,7 @@ export function validateObs(input) {
382
472
  return { errors: ["Expected an object in the ObsBrief shape"], warnings: [] };
383
473
  }
384
474
  const o = input;
385
- const bag = new Map();
475
+ const bag = newBag();
386
476
  const warnings = [];
387
477
  const str = (v) => (typeof v === "string" ? v : undefined);
388
478
  const set = (key, v) => {
@@ -422,6 +512,26 @@ export function validateObs(input) {
422
512
  if (Array.isArray(o.keep))
423
513
  for (const k of o.keep)
424
514
  set("obs:keep", k);
515
+ if (Array.isArray(o.highlights)) {
516
+ for (const h of o.highlights) {
517
+ if (!h || typeof h !== "object")
518
+ continue;
519
+ const rec = h;
520
+ const t = str(rec.text);
521
+ if (t === undefined) {
522
+ warnings.push("A highlight has no text; it is ignored");
523
+ continue;
524
+ }
525
+ const raw = { text: t };
526
+ const label = str(rec.label);
527
+ if (label !== undefined)
528
+ raw.label = label;
529
+ // The producer saw the page; its verdict is carried through.
530
+ if (typeof rec.inContent === "boolean")
531
+ raw.inContent = rec.inContent;
532
+ bag.highlights.push(raw);
533
+ }
534
+ }
425
535
  if (o.derive && typeof o.derive === "object") {
426
536
  const d = o.derive;
427
537
  set("obs:derive", d.mode);
@@ -438,7 +548,7 @@ export function validateObs(input) {
438
548
  });
439
549
  }
440
550
  // ---------------------------------------------------------------------------
441
- // Feed against page (§13)
551
+ // Feed against page (§14)
442
552
  /** The later obs:modified stands; on a tie, the page stands. */
443
553
  export function mergeObs(page, feed) {
444
554
  if (!page)
@@ -19,6 +19,7 @@
19
19
  }
20
20
  ],
21
21
  "keep": [],
22
+ "highlights": [],
22
23
  "derive": {
23
24
  "mode": "anchor",
24
25
  "translate": true,
@@ -46,6 +47,7 @@
46
47
  }
47
48
  ],
48
49
  "keep": [],
50
+ "highlights": [],
49
51
  "derive": {
50
52
  "mode": "anchor",
51
53
  "translate": true,
@@ -71,6 +73,7 @@
71
73
  }
72
74
  ],
73
75
  "keep": [],
76
+ "highlights": [],
74
77
  "derive": {
75
78
  "mode": "anchor",
76
79
  "translate": true,
@@ -98,6 +101,7 @@
98
101
  }
99
102
  ],
100
103
  "keep": [],
104
+ "highlights": [],
101
105
  "derive": {
102
106
  "mode": "anchor",
103
107
  "translate": true,
@@ -123,6 +127,7 @@
123
127
  }
124
128
  ],
125
129
  "keep": [],
130
+ "highlights": [],
126
131
  "derive": {
127
132
  "mode": "anchor",
128
133
  "translate": true,
@@ -150,6 +155,7 @@
150
155
  }
151
156
  ],
152
157
  "keep": [],
158
+ "highlights": [],
153
159
  "derive": {
154
160
  "mode": "anchor",
155
161
  "translate": true,
@@ -178,6 +184,7 @@
178
184
  }
179
185
  ],
180
186
  "keep": [],
187
+ "highlights": [],
181
188
  "derive": {
182
189
  "mode": "verbatim",
183
190
  "translate": true,
@@ -203,6 +210,7 @@
203
210
  }
204
211
  ],
205
212
  "keep": [],
213
+ "highlights": [],
206
214
  "derive": {
207
215
  "mode": "anchor",
208
216
  "translate": true,
@@ -234,6 +242,7 @@
234
242
  "keep": [
235
243
  "Anyone may take vegetables home once the harvest begins."
236
244
  ],
245
+ "highlights": [],
237
246
  "derive": {
238
247
  "mode": "anchor",
239
248
  "translate": true,
@@ -270,6 +279,17 @@
270
279
  "keep": [
271
280
  "Anyone may take vegetables home once the harvest begins."
272
281
  ],
282
+ "highlights": [
283
+ {
284
+ "text": "Anyone may take vegetables home once the harvest begins.",
285
+ "words": 9,
286
+ "label": "On the harvest"
287
+ },
288
+ {
289
+ "text": "Children from the school will help on the first Saturday.",
290
+ "words": 10
291
+ }
292
+ ],
273
293
  "derive": {
274
294
  "mode": "anchor",
275
295
  "translate": false,
@@ -296,6 +316,7 @@
296
316
  }
297
317
  ],
298
318
  "keep": [],
319
+ "highlights": [],
299
320
  "derive": {
300
321
  "mode": "anchor",
301
322
  "translate": true,
@@ -323,6 +344,216 @@
323
344
  }
324
345
  ],
325
346
  "keep": [],
347
+ "highlights": [],
348
+ "derive": {
349
+ "mode": "anchor",
350
+ "translate": true,
351
+ "combine": true
352
+ },
353
+ "preference": "summary",
354
+ "lang": "en"
355
+ },
356
+ "errors": [],
357
+ "warnings": []
358
+ },
359
+ "highlight-label-orphan.html": {
360
+ "brief": {
361
+ "version": 1,
362
+ "modified": "2026-11-08T14:00:00Z",
363
+ "describesThisPage": true,
364
+ "summaries": [
365
+ {
366
+ "tier": "medium",
367
+ "text": "The garden club met on Tuesday to plan the spring planting. Volunteers agreed to water the beds every second morning. Most of the seeds came from last year's harvest. The committee chose tomatoes, beans and three kinds of squash. A new fence will keep the rabbits away from the lettuce. Members who cannot dig offered to label the rows instead. The treasurer reported enough money for compost and tools. Children from the school will help on the first Saturday. Anyone may take vegetables home once the harvest begins. The next meeting is set for the first week of the month. The garden club met on Tuesday to plan the spring planting. Volunteers agreed to water the beds every second morning. Most of the seeds came from last year's harvest. The committee chose tomatoes, beans and three kinds of squash. A new fence will keep the rabbits away from the lettuce. Members who cannot dig offered to label the rows instead. The treasurer reported enough money for compost and tools. Children from the school will help on the first Saturday. Anyone may take vegetables home once the harvest begins. The next meeting is set for the first week of the month. The garden club met on Tuesday to plan the spring planting. Volunteers agreed to water the beds every second morning. Most of the seeds came from last year's harvest. The committee chose tomatoes, beans and three kinds of squash. A new fence will keep the rabbits away from the lettuce. Members who cannot dig offered to label the rows instead. The treasurer reported enough money for compost and tools. Children from the school will help on the first Saturday. Anyone.",
368
+ "words": 280,
369
+ "from": "inferred"
370
+ }
371
+ ],
372
+ "keep": [],
373
+ "highlights": [
374
+ {
375
+ "text": "Anyone may take vegetables home once the harvest begins.",
376
+ "words": 9,
377
+ "inContent": true
378
+ }
379
+ ],
380
+ "derive": {
381
+ "mode": "anchor",
382
+ "translate": true,
383
+ "combine": true
384
+ },
385
+ "preference": "summary",
386
+ "lang": "en"
387
+ },
388
+ "errors": [],
389
+ "warnings": [
390
+ "obs:highlight:label before any obs:highlight; it is ignored (§3.1)"
391
+ ]
392
+ },
393
+ "highlight-limits.html": {
394
+ "brief": {
395
+ "version": 1,
396
+ "modified": "2026-11-08T14:00:00Z",
397
+ "describesThisPage": true,
398
+ "summaries": [
399
+ {
400
+ "tier": "medium",
401
+ "text": "The garden club met on Tuesday to plan the spring planting. Volunteers agreed to water the beds every second morning. Most of the seeds came from last year's harvest. The committee chose tomatoes, beans and three kinds of squash. A new fence will keep the rabbits away from the lettuce. Members who cannot dig offered to label the rows instead. The treasurer reported enough money for compost and tools. Children from the school will help on the first Saturday. Anyone may take vegetables home once the harvest begins. The next meeting is set for the first week of the month. The garden club met on Tuesday to plan the spring planting. Volunteers agreed to water the beds every second morning. Most of the seeds came from last year's harvest. The committee chose tomatoes, beans and three kinds of squash. A new fence will keep the rabbits away from the lettuce. Members who cannot dig offered to label the rows instead. The treasurer reported enough money for compost and tools. Children from the school will help on the first Saturday. Anyone may take vegetables home once the harvest begins. The next meeting is set for the first week of the month. The garden club met on Tuesday to plan the spring planting. Volunteers agreed to water the beds every second morning. Most of the seeds came from last year's harvest. The committee chose tomatoes, beans and three kinds of squash. A new fence will keep the rabbits away from the lettuce. Members who cannot dig offered to label the rows instead. The treasurer reported enough money for compost and tools. Children from the school will help on the first Saturday. Anyone.",
402
+ "words": 280,
403
+ "from": "inferred"
404
+ }
405
+ ],
406
+ "keep": [],
407
+ "highlights": [
408
+ {
409
+ "text": "The garden club met on Tuesday to plan the spring planting.",
410
+ "words": 11,
411
+ "inContent": true
412
+ },
413
+ {
414
+ "text": "The garden club met on Tuesday to plan the spring planting. Volunteers agreed to water the beds every second morning. Most of the seeds came from last year's harvest. The committee chose tomatoes, beans and three kinds of squash. A new fence will keep the rabbits away from the lettuce. Members who cannot dig offered to label the rows instead. The treasurer reported enough money for compost and tools.",
415
+ "words": 69,
416
+ "inContent": true
417
+ },
418
+ {
419
+ "text": "Most of the seeds came from last year's harvest.",
420
+ "words": 9,
421
+ "inContent": true
422
+ },
423
+ {
424
+ "text": "The committee chose tomatoes, beans and three kinds of squash.",
425
+ "words": 10,
426
+ "inContent": true
427
+ },
428
+ {
429
+ "text": "Members who cannot dig offered to label the rows instead.",
430
+ "words": 10,
431
+ "inContent": true
432
+ }
433
+ ],
434
+ "derive": {
435
+ "mode": "anchor",
436
+ "translate": true,
437
+ "combine": true
438
+ },
439
+ "preference": "summary",
440
+ "lang": "en"
441
+ },
442
+ "errors": [],
443
+ "warnings": [
444
+ "obs:highlight appears 7 times; only the first 5 are used (§3)",
445
+ "obs:highlight #2: 69 words, over its size of 60 (§4)"
446
+ ]
447
+ },
448
+ "highlight-not-in-text.html": {
449
+ "brief": {
450
+ "version": 1,
451
+ "modified": "2026-11-08T14:00:00Z",
452
+ "describesThisPage": true,
453
+ "summaries": [
454
+ {
455
+ "tier": "medium",
456
+ "text": "The garden club met on Tuesday to plan the spring planting. Volunteers agreed to water the beds every second morning. Most of the seeds came from last year's harvest. The committee chose tomatoes, beans and three kinds of squash. A new fence will keep the rabbits away from the lettuce. Members who cannot dig offered to label the rows instead. The treasurer reported enough money for compost and tools. Children from the school will help on the first Saturday. Anyone may take vegetables home once the harvest begins. The next meeting is set for the first week of the month. The garden club met on Tuesday to plan the spring planting. Volunteers agreed to water the beds every second morning. Most of the seeds came from last year's harvest. The committee chose tomatoes, beans and three kinds of squash. A new fence will keep the rabbits away from the lettuce. Members who cannot dig offered to label the rows instead. The treasurer reported enough money for compost and tools. Children from the school will help on the first Saturday. Anyone may take vegetables home once the harvest begins. The next meeting is set for the first week of the month. The garden club met on Tuesday to plan the spring planting. Volunteers agreed to water the beds every second morning. Most of the seeds came from last year's harvest. The committee chose tomatoes, beans and three kinds of squash. A new fence will keep the rabbits away from the lettuce. Members who cannot dig offered to label the rows instead. The treasurer reported enough money for compost and tools. Children from the school will help on the first Saturday. Anyone.",
457
+ "words": 280,
458
+ "from": "inferred"
459
+ }
460
+ ],
461
+ "keep": [],
462
+ "highlights": [
463
+ {
464
+ "text": "Most of the seeds came from last year’s harvest.",
465
+ "words": 9,
466
+ "inContent": true
467
+ },
468
+ {
469
+ "text": "The fence — a new one — keeps the rabbits out.",
470
+ "words": 11,
471
+ "inContent": true
472
+ },
473
+ {
474
+ "text": "The club bought a tractor for the heavy digging.",
475
+ "words": 9,
476
+ "inContent": false
477
+ }
478
+ ],
479
+ "derive": {
480
+ "mode": "anchor",
481
+ "translate": true,
482
+ "combine": true
483
+ },
484
+ "preference": "summary",
485
+ "lang": "en"
486
+ },
487
+ "errors": [],
488
+ "warnings": [
489
+ "obs:highlight \"The club bought a tractor for…\" is not in the page's text (§7)"
490
+ ]
491
+ },
492
+ "highlight-reserved.html": {
493
+ "brief": {
494
+ "version": 1,
495
+ "modified": "2026-11-08T14:00:00Z",
496
+ "describesThisPage": true,
497
+ "summaries": [
498
+ {
499
+ "tier": "medium",
500
+ "text": "The garden club met on Tuesday to plan the spring planting. Volunteers agreed to water the beds every second morning. Most of the seeds came from last year's harvest. The committee chose tomatoes, beans and three kinds of squash. A new fence will keep the rabbits away from the lettuce. Members who cannot dig offered to label the rows instead. The treasurer reported enough money for compost and tools. Children from the school will help on the first Saturday. Anyone may take vegetables home once the harvest begins. The next meeting is set for the first week of the month. The garden club met on Tuesday to plan the spring planting. Volunteers agreed to water the beds every second morning. Most of the seeds came from last year's harvest. The committee chose tomatoes, beans and three kinds of squash. A new fence will keep the rabbits away from the lettuce. Members who cannot dig offered to label the rows instead. The treasurer reported enough money for compost and tools. Children from the school will help on the first Saturday. Anyone may take vegetables home once the harvest begins. The next meeting is set for the first week of the month. The garden club met on Tuesday to plan the spring planting. Volunteers agreed to water the beds every second morning. Most of the seeds came from last year's harvest. The committee chose tomatoes, beans and three kinds of squash. A new fence will keep the rabbits away from the lettuce. Members who cannot dig offered to label the rows instead. The treasurer reported enough money for compost and tools. Children from the school will help on the first Saturday. Anyone.",
501
+ "words": 280,
502
+ "from": "inferred"
503
+ }
504
+ ],
505
+ "keep": [],
506
+ "highlights": [
507
+ {
508
+ "text": "Anyone may take vegetables home once the harvest begins.",
509
+ "words": 9,
510
+ "inContent": true
511
+ }
512
+ ],
513
+ "derive": {
514
+ "mode": "anchor",
515
+ "translate": true,
516
+ "combine": true
517
+ },
518
+ "preference": "summary",
519
+ "lang": "en"
520
+ },
521
+ "errors": [],
522
+ "warnings": []
523
+ },
524
+ "highlights.html": {
525
+ "brief": {
526
+ "version": 1,
527
+ "modified": "2026-11-08T14:00:00Z",
528
+ "describesThisPage": true,
529
+ "summaries": [
530
+ {
531
+ "tier": "medium",
532
+ "text": "The garden club met on Tuesday to plan the spring planting. Volunteers agreed to water the beds every second morning. Most of the seeds came from last year's harvest. The committee chose tomatoes, beans and three kinds of squash. A new fence will keep the rabbits away from the lettuce. Members who cannot dig offered to label the rows instead. The treasurer reported enough money for compost and tools. Children from the school will help on the first Saturday. Anyone may take vegetables home once the harvest begins. The next meeting is set for the first week of the month. The garden club met on Tuesday to plan the spring planting. Volunteers agreed to water the beds every second morning. Most of the seeds came from last year's harvest. The committee chose tomatoes, beans and three kinds of squash. A new fence will keep the rabbits away from the lettuce. Members who cannot dig offered to label the rows instead. The treasurer reported enough money for compost and tools. Children from the school will help on the first Saturday. Anyone may take vegetables home once the harvest begins. The next meeting is set for the first week of the month. The garden club met on Tuesday to plan the spring planting. Volunteers agreed to water the beds every second morning. Most of the seeds came from last year's harvest. The committee chose tomatoes, beans and three kinds of squash. A new fence will keep the rabbits away from the lettuce. Members who cannot dig offered to label the rows instead. The treasurer reported enough money for compost and tools. Children from the school will help on the first Saturday. Anyone.",
533
+ "words": 280,
534
+ "from": "inferred"
535
+ }
536
+ ],
537
+ "keep": [],
538
+ "highlights": [
539
+ {
540
+ "text": "Anyone may take vegetables home once the harvest begins.",
541
+ "words": 9,
542
+ "label": "On the harvest",
543
+ "inContent": true
544
+ },
545
+ {
546
+ "text": "Volunteers agreed to water the beds every second morning.",
547
+ "words": 9,
548
+ "inContent": true
549
+ },
550
+ {
551
+ "text": "A new fence will keep the rabbits away from the lettuce.",
552
+ "words": 11,
553
+ "label": "On rabbits",
554
+ "inContent": true
555
+ }
556
+ ],
326
557
  "derive": {
327
558
  "mode": "anchor",
328
559
  "translate": true,
@@ -348,6 +579,7 @@
348
579
  }
349
580
  ],
350
581
  "keep": [],
582
+ "highlights": [],
351
583
  "derive": {
352
584
  "mode": "anchor",
353
585
  "translate": true,
@@ -377,6 +609,7 @@
377
609
  "The garden club met on Tuesday to plan the spring planting. Volunteers agreed to water the beds every second morning. Most of the seeds came from last year's harvest. The committee chose tomatoes, beans and three kinds of squash. A new fence will keep the rabbits away from the lettuce. Members who cannot dig offered.",
378
610
  "Children from the school help on the first Saturday."
379
611
  ],
612
+ "highlights": [],
380
613
  "derive": {
381
614
  "mode": "anchor",
382
615
  "translate": true,
@@ -405,6 +638,7 @@
405
638
  }
406
639
  ],
407
640
  "keep": [],
641
+ "highlights": [],
408
642
  "derive": {
409
643
  "mode": "anchor",
410
644
  "translate": true,
@@ -430,6 +664,7 @@
430
664
  }
431
665
  ],
432
666
  "keep": [],
667
+ "highlights": [],
433
668
  "derive": {
434
669
  "mode": "anchor",
435
670
  "translate": true,
@@ -440,8 +675,8 @@
440
675
  },
441
676
  "errors": [],
442
677
  "warnings": [
443
- "obs:summary: HTML removed; values are plain text (§8)",
444
- "obs:summary: contains a URL; summaries should not (§8)"
678
+ "obs:summary: HTML removed; values are plain text (§9)",
679
+ "obs:summary: contains a URL; summaries should not (§9)"
445
680
  ]
446
681
  },
447
682
  "missing-modified.html": {
@@ -470,6 +705,7 @@
470
705
  }
471
706
  ],
472
707
  "keep": [],
708
+ "highlights": [],
473
709
  "derive": {
474
710
  "mode": "anchor",
475
711
  "translate": true,
@@ -495,6 +731,7 @@
495
731
  }
496
732
  ],
497
733
  "keep": [],
734
+ "highlights": [],
498
735
  "derive": {
499
736
  "mode": "anchor",
500
737
  "translate": true,
@@ -528,6 +765,7 @@
528
765
  }
529
766
  ],
530
767
  "keep": [],
768
+ "highlights": [],
531
769
  "derive": {
532
770
  "mode": "anchor",
533
771
  "translate": true,
@@ -559,6 +797,7 @@
559
797
  }
560
798
  ],
561
799
  "keep": [],
800
+ "highlights": [],
562
801
  "derive": {
563
802
  "mode": "anchor",
564
803
  "translate": true,
@@ -584,6 +823,7 @@
584
823
  }
585
824
  ],
586
825
  "keep": [],
826
+ "highlights": [],
587
827
  "derive": {
588
828
  "mode": "anchor",
589
829
  "translate": true,
@@ -609,6 +849,7 @@
609
849
  }
610
850
  ],
611
851
  "keep": [],
852
+ "highlights": [],
612
853
  "derive": {
613
854
  "mode": "anchor",
614
855
  "translate": true,
@@ -642,6 +883,7 @@
642
883
  }
643
884
  ],
644
885
  "keep": [],
886
+ "highlights": [],
645
887
  "derive": {
646
888
  "mode": "anchor",
647
889
  "translate": true,
@@ -667,6 +909,7 @@
667
909
  }
668
910
  ],
669
911
  "keep": [],
912
+ "highlights": [],
670
913
  "derive": {
671
914
  "mode": "anchor",
672
915
  "translate": true,
@@ -694,6 +937,7 @@
694
937
  }
695
938
  ],
696
939
  "keep": [],
940
+ "highlights": [],
697
941
  "derive": {
698
942
  "mode": "anchor",
699
943
  "translate": true,
@@ -720,6 +964,7 @@
720
964
  }
721
965
  ],
722
966
  "keep": [],
967
+ "highlights": [],
723
968
  "derive": {
724
969
  "mode": "anchor",
725
970
  "translate": true,
@@ -762,6 +1007,19 @@
762
1007
  "Volunteers water the beds every second morning.",
763
1008
  "Children from the school help on the first Saturday."
764
1009
  ],
1010
+ "highlights": [
1011
+ {
1012
+ "text": "Anyone may take vegetables home once the harvest begins.",
1013
+ "words": 9,
1014
+ "label": "On the harvest",
1015
+ "inContent": true
1016
+ },
1017
+ {
1018
+ "text": "Children from the school will help on the first Saturday.",
1019
+ "words": 10,
1020
+ "inContent": true
1021
+ }
1022
+ ],
765
1023
  "derive": {
766
1024
  "mode": "anchor",
767
1025
  "translate": false,
@@ -790,6 +1048,7 @@
790
1048
  }
791
1049
  ],
792
1050
  "keep": [],
1051
+ "highlights": [],
793
1052
  "derive": {
794
1053
  "mode": "anchor",
795
1054
  "translate": true,
@@ -12,6 +12,8 @@
12
12
  <obs:summary>The garden club met on Tuesday to plan the spring planting. Volunteers agreed to water the beds every second morning. Most of the seeds came from last year's harvest. The committee chose tomatoes, beans and three kinds of squash. A new fence will keep the rabbits away from the lettuce. Members who cannot dig offered to label the rows instead. The treasurer reported enough money for compost and tools. Children from the school will help on the first Saturday. Anyone may take vegetables home once the harvest begins. The next meeting is set for the first week of the month. The garden club met on Tuesday to plan the spring planting. Volunteers agreed to water the beds every second morning. Most of the seeds came from last year's harvest. The committee chose tomatoes, beans and three kinds of squash. A new fence will keep the rabbits away from the lettuce. Members who cannot dig offered to label the rows instead. The treasurer reported enough money for compost and tools. Children from the school will help on the first Saturday. Anyone may take vegetables home once the harvest begins. The next meeting is set for the first week of the month. The garden club met on Tuesday to plan the spring planting. Volunteers agreed to water the beds every second morning. Most of the seeds came from last year's harvest. The committee chose tomatoes, beans and three kinds of squash. A new fence will keep the rabbits away from the lettuce. Members who cannot dig offered to label the rows instead. The treasurer reported enough money for compost and tools. Children from the school will help on the first Saturday. Anyone.</obs:summary>
13
13
  <obs:summary length="short">The garden club met on Tuesday to plan the spring planting. Volunteers agreed to water the beds every second morning. Most of the seeds came from last year's harvest. The committee chose tomatoes, beans and three kinds of squash. A new fence will keep the rabbits away from the lettuce. Members who cannot dig offered to label the rows instead.</obs:summary>
14
14
  <obs:keep>Anyone may take vegetables home once the harvest begins.</obs:keep>
15
+ <obs:highlight label="On the harvest">Anyone may take vegetables home once the harvest begins.</obs:highlight>
16
+ <obs:highlight>Children from the school will help on the first Saturday.</obs:highlight>
15
17
  <obs:derive translate="no">anchor</obs:derive>
16
18
  <obs:attribution>From the Garden Club newsletter</obs:attribution>
17
19
  </item>
@@ -0,0 +1,15 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head prefix="obs: https://openbriefingstandard.org/ns#">
4
+ <meta charset="utf-8">
5
+ <title>Garden Club Notes: highlight-label-orphan</title>
6
+ <link rel="canonical" href="https://example.test/highlight-label-orphan/">
7
+ <meta property="obs:modified" content="2026-11-08T14:00:00Z">
8
+ <meta property="obs:summary" content="The garden club met on Tuesday to plan the spring planting. Volunteers agreed to water the beds every second morning. Most of the seeds came from last year's harvest. The committee chose tomatoes, beans and three kinds of squash. A new fence will keep the rabbits away from the lettuce. Members who cannot dig offered to label the rows instead. The treasurer reported enough money for compost and tools. Children from the school will help on the first Saturday. Anyone may take vegetables home once the harvest begins. The next meeting is set for the first week of the month. The garden club met on Tuesday to plan the spring planting. Volunteers agreed to water the beds every second morning. Most of the seeds came from last year's harvest. The committee chose tomatoes, beans and three kinds of squash. A new fence will keep the rabbits away from the lettuce. Members who cannot dig offered to label the rows instead. The treasurer reported enough money for compost and tools. Children from the school will help on the first Saturday. Anyone may take vegetables home once the harvest begins. The next meeting is set for the first week of the month. The garden club met on Tuesday to plan the spring planting. Volunteers agreed to water the beds every second morning. Most of the seeds came from last year's harvest. The committee chose tomatoes, beans and three kinds of squash. A new fence will keep the rabbits away from the lettuce. Members who cannot dig offered to label the rows instead. The treasurer reported enough money for compost and tools. Children from the school will help on the first Saturday. Anyone.">
9
+ <meta property="obs:highlight:label" content="A label with nothing before it">
10
+ <meta property="obs:highlight" content="Anyone may take vegetables home once the harvest begins.">
11
+ </head>
12
+ <body>
13
+ <article><h1>Garden Club Notes</h1><p>The garden club met on Tuesday to plan the spring planting. Volunteers agreed to water the beds every second morning. Most of the seeds came from last year's harvest. The committee chose tomatoes, beans and three kinds of squash. A new fence will keep the rabbits away from the lettuce. Members who cannot dig offered to label the rows instead. The treasurer reported enough money for compost and tools. Children from the school will help on the first Saturday. Anyone may take vegetables home once the harvest begins. The next meeting is set for the first week of the month.</p></article>
14
+ </body>
15
+ </html>
@@ -0,0 +1,20 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head prefix="obs: https://openbriefingstandard.org/ns#">
4
+ <meta charset="utf-8">
5
+ <title>Garden Club Notes: highlight-limits</title>
6
+ <link rel="canonical" href="https://example.test/highlight-limits/">
7
+ <meta property="obs:modified" content="2026-11-08T14:00:00Z">
8
+ <meta property="obs:summary" content="The garden club met on Tuesday to plan the spring planting. Volunteers agreed to water the beds every second morning. Most of the seeds came from last year's harvest. The committee chose tomatoes, beans and three kinds of squash. A new fence will keep the rabbits away from the lettuce. Members who cannot dig offered to label the rows instead. The treasurer reported enough money for compost and tools. Children from the school will help on the first Saturday. Anyone may take vegetables home once the harvest begins. The next meeting is set for the first week of the month. The garden club met on Tuesday to plan the spring planting. Volunteers agreed to water the beds every second morning. Most of the seeds came from last year's harvest. The committee chose tomatoes, beans and three kinds of squash. A new fence will keep the rabbits away from the lettuce. Members who cannot dig offered to label the rows instead. The treasurer reported enough money for compost and tools. Children from the school will help on the first Saturday. Anyone may take vegetables home once the harvest begins. The next meeting is set for the first week of the month. The garden club met on Tuesday to plan the spring planting. Volunteers agreed to water the beds every second morning. Most of the seeds came from last year's harvest. The committee chose tomatoes, beans and three kinds of squash. A new fence will keep the rabbits away from the lettuce. Members who cannot dig offered to label the rows instead. The treasurer reported enough money for compost and tools. Children from the school will help on the first Saturday. Anyone.">
9
+ <meta property="obs:highlight" content="The garden club met on Tuesday to plan the spring planting.">
10
+ <meta property="obs:highlight" content="The garden club met on Tuesday to plan the spring planting. Volunteers agreed to water the beds every second morning. Most of the seeds came from last year's harvest. The committee chose tomatoes, beans and three kinds of squash. A new fence will keep the rabbits away from the lettuce. Members who cannot dig offered to label the rows instead. The treasurer reported enough money for compost and tools.">
11
+ <meta property="obs:highlight" content="Most of the seeds came from last year's harvest.">
12
+ <meta property="obs:highlight" content="The committee chose tomatoes, beans and three kinds of squash.">
13
+ <meta property="obs:highlight" content="Members who cannot dig offered to label the rows instead.">
14
+ <meta property="obs:highlight" content="A sixth highlight that is dropped.">
15
+ <meta property="obs:highlight" content="A seventh highlight that is dropped.">
16
+ </head>
17
+ <body>
18
+ <article><h1>Garden Club Notes</h1><p>The garden club met on Tuesday to plan the spring planting. Volunteers agreed to water the beds every second morning. Most of the seeds came from last year's harvest. The committee chose tomatoes, beans and three kinds of squash. A new fence will keep the rabbits away from the lettuce. Members who cannot dig offered to label the rows instead. The treasurer reported enough money for compost and tools. Children from the school will help on the first Saturday. Anyone may take vegetables home once the harvest begins. The next meeting is set for the first week of the month.</p></article>
19
+ </body>
20
+ </html>
@@ -0,0 +1,17 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head prefix="obs: https://openbriefingstandard.org/ns#">
4
+ <meta charset="utf-8">
5
+ <title>Garden Club Notes: highlight-not-in-text</title>
6
+ <link rel="canonical" href="https://example.test/highlight-not-in-text/">
7
+ <meta property="obs:modified" content="2026-11-08T14:00:00Z">
8
+ <meta property="obs:summary" content="The garden club met on Tuesday to plan the spring planting. Volunteers agreed to water the beds every second morning. Most of the seeds came from last year's harvest. The committee chose tomatoes, beans and three kinds of squash. A new fence will keep the rabbits away from the lettuce. Members who cannot dig offered to label the rows instead. The treasurer reported enough money for compost and tools. Children from the school will help on the first Saturday. Anyone may take vegetables home once the harvest begins. The next meeting is set for the first week of the month. The garden club met on Tuesday to plan the spring planting. Volunteers agreed to water the beds every second morning. Most of the seeds came from last year's harvest. The committee chose tomatoes, beans and three kinds of squash. A new fence will keep the rabbits away from the lettuce. Members who cannot dig offered to label the rows instead. The treasurer reported enough money for compost and tools. Children from the school will help on the first Saturday. Anyone may take vegetables home once the harvest begins. The next meeting is set for the first week of the month. The garden club met on Tuesday to plan the spring planting. Volunteers agreed to water the beds every second morning. Most of the seeds came from last year's harvest. The committee chose tomatoes, beans and three kinds of squash. A new fence will keep the rabbits away from the lettuce. Members who cannot dig offered to label the rows instead. The treasurer reported enough money for compost and tools. Children from the school will help on the first Saturday. Anyone.">
9
+ <meta property="obs:highlight" content="Most of the seeds came from last year’s harvest.">
10
+ <meta property="obs:highlight" content="The fence — a new one — keeps the rabbits out.">
11
+ <meta property="obs:highlight" content="The club bought a tractor for the heavy digging.">
12
+ </head>
13
+ <body>
14
+ <article><h1>Garden Club Notes</h1><p>The garden club met on Tuesday to plan the spring planting. Volunteers agreed to water the beds every second morning. Most of the seeds came from last year's harvest. The committee chose tomatoes, beans and three kinds of squash. A new fence will keep the rabbits away from the lettuce. Members who cannot dig offered to label the rows instead. The treasurer reported enough money for compost and tools. Children from the school will help on the first Saturday. Anyone may take vegetables home once the harvest begins. The next meeting is set for the first week of the month.</p></article>
15
+ <p>The fence - a new one - keeps the rabbits out.</p>
16
+ </body>
17
+ </html>
@@ -0,0 +1,16 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head prefix="obs: https://openbriefingstandard.org/ns#">
4
+ <meta charset="utf-8">
5
+ <title>Garden Club Notes: highlight-reserved</title>
6
+ <link rel="canonical" href="https://example.test/highlight-reserved/">
7
+ <meta property="obs:modified" content="2026-11-08T14:00:00Z">
8
+ <meta property="obs:summary" content="The garden club met on Tuesday to plan the spring planting. Volunteers agreed to water the beds every second morning. Most of the seeds came from last year's harvest. The committee chose tomatoes, beans and three kinds of squash. A new fence will keep the rabbits away from the lettuce. Members who cannot dig offered to label the rows instead. The treasurer reported enough money for compost and tools. Children from the school will help on the first Saturday. Anyone may take vegetables home once the harvest begins. The next meeting is set for the first week of the month. The garden club met on Tuesday to plan the spring planting. Volunteers agreed to water the beds every second morning. Most of the seeds came from last year's harvest. The committee chose tomatoes, beans and three kinds of squash. A new fence will keep the rabbits away from the lettuce. Members who cannot dig offered to label the rows instead. The treasurer reported enough money for compost and tools. Children from the school will help on the first Saturday. Anyone may take vegetables home once the harvest begins. The next meeting is set for the first week of the month. The garden club met on Tuesday to plan the spring planting. Volunteers agreed to water the beds every second morning. Most of the seeds came from last year's harvest. The committee chose tomatoes, beans and three kinds of squash. A new fence will keep the rabbits away from the lettuce. Members who cannot dig offered to label the rows instead. The treasurer reported enough money for compost and tools. Children from the school will help on the first Saturday. Anyone.">
9
+ <meta property="obs:highlight" content="Anyone may take vegetables home once the harvest begins.">
10
+ <meta property="obs:highlight:start" content="00:01:12">
11
+ <meta property="obs:highlight:end" content="00:01:30">
12
+ </head>
13
+ <body>
14
+ <article><h1>Garden Club Notes</h1><p>The garden club met on Tuesday to plan the spring planting. Volunteers agreed to water the beds every second morning. Most of the seeds came from last year's harvest. The committee chose tomatoes, beans and three kinds of squash. A new fence will keep the rabbits away from the lettuce. Members who cannot dig offered to label the rows instead. The treasurer reported enough money for compost and tools. Children from the school will help on the first Saturday. Anyone may take vegetables home once the harvest begins. The next meeting is set for the first week of the month.</p></article>
15
+ </body>
16
+ </html>
@@ -0,0 +1,18 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head prefix="obs: https://openbriefingstandard.org/ns#">
4
+ <meta charset="utf-8">
5
+ <title>Garden Club Notes: highlights</title>
6
+ <link rel="canonical" href="https://example.test/highlights/">
7
+ <meta property="obs:modified" content="2026-11-08T14:00:00Z">
8
+ <meta property="obs:summary" content="The garden club met on Tuesday to plan the spring planting. Volunteers agreed to water the beds every second morning. Most of the seeds came from last year's harvest. The committee chose tomatoes, beans and three kinds of squash. A new fence will keep the rabbits away from the lettuce. Members who cannot dig offered to label the rows instead. The treasurer reported enough money for compost and tools. Children from the school will help on the first Saturday. Anyone may take vegetables home once the harvest begins. The next meeting is set for the first week of the month. The garden club met on Tuesday to plan the spring planting. Volunteers agreed to water the beds every second morning. Most of the seeds came from last year's harvest. The committee chose tomatoes, beans and three kinds of squash. A new fence will keep the rabbits away from the lettuce. Members who cannot dig offered to label the rows instead. The treasurer reported enough money for compost and tools. Children from the school will help on the first Saturday. Anyone may take vegetables home once the harvest begins. The next meeting is set for the first week of the month. The garden club met on Tuesday to plan the spring planting. Volunteers agreed to water the beds every second morning. Most of the seeds came from last year's harvest. The committee chose tomatoes, beans and three kinds of squash. A new fence will keep the rabbits away from the lettuce. Members who cannot dig offered to label the rows instead. The treasurer reported enough money for compost and tools. Children from the school will help on the first Saturday. Anyone.">
9
+ <meta property="obs:highlight" content="Anyone may take vegetables home once the harvest begins.">
10
+ <meta property="obs:highlight:label" content="On the harvest">
11
+ <meta property="obs:highlight" content="Volunteers agreed to water the beds every second morning.">
12
+ <meta property="obs:highlight" content="A new fence will keep the rabbits away from the lettuce.">
13
+ <meta property="obs:highlight:label" content="On rabbits">
14
+ </head>
15
+ <body>
16
+ <article><h1>Garden Club Notes</h1><p>The garden club met on Tuesday to plan the spring planting. Volunteers agreed to water the beds every second morning. Most of the seeds came from last year's harvest. The committee chose tomatoes, beans and three kinds of squash. A new fence will keep the rabbits away from the lettuce. Members who cannot dig offered to label the rows instead. The treasurer reported enough money for compost and tools. Children from the school will help on the first Saturday. Anyone may take vegetables home once the harvest begins. The next meeting is set for the first week of the month.</p></article>
17
+ </body>
18
+ </html>
@@ -13,6 +13,9 @@
13
13
  <meta property="obs:keep" content="Anyone may take vegetables home once the harvest begins.">
14
14
  <meta property="obs:keep" content="Volunteers water the beds every second morning.">
15
15
  <meta property="obs:keep" content="Children from the school help on the first Saturday.">
16
+ <meta property="obs:highlight" content="Anyone may take vegetables home once the harvest begins.">
17
+ <meta property="obs:highlight:label" content="On the harvest">
18
+ <meta property="obs:highlight" content="Children from the school will help on the first Saturday.">
16
19
  <meta property="obs:attribution" content="From the Garden Club newsletter">
17
20
  <meta property="obs:derive" content="anchor">
18
21
  <meta property="obs:derive:translate" content="no">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openbriefing",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Open Briefing Standard (OBS): the reference parser and checker for publishers' own summaries.",
5
5
  "keywords": [
6
6
  "open-briefing-standard",