Skip to content

Commit

Permalink
Merge pull request #215 from braxtonhall/mapped-tags
Browse files Browse the repository at this point in the history
Allow librarians to map all the tags in a sheet
  • Loading branch information
braxtonhall authored Dec 24, 2022
2 parents ea43b7b + 0ca5f9a commit 11b9adf
Show file tree
Hide file tree
Showing 2 changed files with 35 additions and 9 deletions.
12 changes: 12 additions & 0 deletions docs/librarian/tag-index.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ The Tag Index _Index_ is used to tell Better Library Thing where all the tables
1. Column `A` _must_ be the name of a sheet where there are tags
1. Column `B` _must_ be the top left cell in that sheet where there are tags
1. Column `C` _must_ be the width of the table in that sheet that contains tags
1. Column `D` is optional, and can [remap tags](#tag-remapping).

Example sheet with Tags:

Expand All @@ -47,3 +48,14 @@ Example sheet with Tags:
Example Tag Index _Index_:

<img src="../img/tag-index/tag-index-index.png" alt="Example Tag Index Index">

### Tag Remapping

A tag remapper is a string which transforms tags as they are read by Better LibraryThing. A tag remapper _must_ contain `$TAG`.
Tag remapping allows a Librarian to quickly add many tags that are transformations of existing tags without editing the existing spreadsheet.

For example, given a `Colours` sheet containing the tags `Red`, `Green`, and `Blue`, and the tag remapper `$TAG author`,
Better LibraryThing will artificially inflate the Tag Index with the new tags `Red author`, `Green author`, and `Blue author`
when performing tag validation or tag auto-completion.

Created for VBL with the intention of Librarians remapping the `Identity` sheet with `$TAG author`.
32 changes: 23 additions & 9 deletions src/ts/content/adapters/tags/getTags.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
import {makeCache} from "../../../common/util/cache";
import {TagSearchOptions, TagTrees} from "./types";
import Sheets, {Range} from "../sheets";
import Sheets, {Range, ValueRange} from "../sheets";
import {parseTree} from "./parseTags";
import {incrementColumnBy} from "../sheets/util";

declare const SPREADSHEET_ID: string; // Declared in webpack DefinePlugin

type TagMapper = `${string}$TAG${string}`;
type MappedRange = {range: Range; mapper: TagMapper};

const META_TAG_SHEET = "Tag Index Index";
/**
* The Meta Tag Sheet stores information about where tags are in the spreadsheet
* Each row is of the form
* | SHEET | TOP_LEFT | WIDTH |
* | SHEET | TOP_LEFT | WIDTH | MAPPER? | AS? |
* where
* SHEET is the name of a sheet in the spreadsheet where tags live
* TOP_LEFT is the top left most cell in the sheet the contains a tag
* WIDTH is how many columns contain tags
* MAPPER is how tags should be mapped to after being consumed by BLT
* mapped tags only exist dynamically in BLT
* AS is how the (possibly artificial) sheet should be named by BLT
*
* Example:
* | Identity | A2 | 2 |
Expand All @@ -27,25 +33,33 @@ const {asyncCached, setCache} = makeCache<TagTrees>();
const rowIsRange = ([, topLeft, width]: string[]): boolean =>
topLeft && width && /^[A-Z]+[0-9]+$/.test(topLeft) && /^[0-9]+$/.test(width);

const rowToRange = ([sheet, topLeft, width]: string[]): Range => {
const rowToMappedRange = ([sheet, topLeft, width, userMapper]: string[]): MappedRange => {
// `left` is a column, for example "B"
const left = topLeft.match(/^[A-Z]+/)[0];
// `right` is the right most column with tags if there are `width` columns of tags
// for example, `left` = "B", `width` = 2, `right` = "C"
const right = incrementColumnBy(left, Number(width) - 1);
return `${sheet}!${topLeft}:${right}`;
const range: Range = `${sheet}!${topLeft}:${right}`;
const mapper: TagMapper = (userMapper ?? "").includes("$TAG") ? (userMapper as TagMapper) : "$TAG";
return {range, mapper};
};

const getTagRanges = async (): Promise<Range[]> => {
const range = Sheets.createRange(META_TAG_SHEET, "A", "C");
const getTagRanges = async (): Promise<MappedRange[]> => {
const range = Sheets.createRange(META_TAG_SHEET, "A", "E");
const response = await Sheets.readRanges(SPREADSHEET_ID, [range]);
return response?.[0].values.filter(rowIsRange).map(rowToRange) ?? [];
return response?.[0].values.filter(rowIsRange).map(rowToMappedRange) ?? [];
};

const getSheetsTags = async (): Promise<string[][]> => {
const ranges = await getTagRanges();
const mappedRanges = await getTagRanges();
const ranges = mappedRanges.map(({range}) => range);
const response = await Sheets.readRanges(SPREADSHEET_ID, ranges);
return response?.flatMap((valueRange) => valueRange.values ?? []) ?? [];
return response?.flatMap((valueRange, index) => mapTags(valueRange, mappedRanges[index].mapper)) ?? [];
};

const mapTags = (valueRange: ValueRange, mapper: TagMapper): string[][] => {
const values = valueRange.values ?? [];
return values.map((row) => row.map((value) => mapper.replaceAll("$TAG", value)));
};

const getTagTrees = async ({noCache}: TagSearchOptions = {noCache: false}) => {
Expand Down

0 comments on commit 11b9adf

Please sign in to comment.