Motivation
The current architecture uses Fiddler (bmPreFiddler) for spreadsheet I/O and stores hyperlinks as HYPERLINK formulas. This creates significant complexity:
- HyperlinkUtils.js (~100+ lines) for formula parsing
- Formula storage in PropertiesService after each save (~50 lines)
- Formula overlay logic on data load
- String manipulation for
=HYPERLINK("url", "text") construction
Key Insight: RichText objects provide native hyperlink support in GAS, making formula complexity unnecessary. However, Fiddler doesn't support RichText, so replacing formulas with RichText requires removing Fiddler.
Net Benefit: Remove ~100-150 lines of code + external dependency
Current Implementation
With Fiddler + Formulas:
// Reading (requires formula overlay from PropertiesService)
const formula = row.Route; // "=HYPERLINK(\"url\", \"text\")"
const { url, text } = HyperlinkUtils.parseHyperlinkFormula(formula);
// Writing (requires formula construction + storage)
row.Route = `=HYPERLINK("${url}", "${text}")`;
// Later: _storeFormulas() saves to PropertiesService
Proposed Implementation
With Native GAS + RichText:
// Reading
const richText = range.getRichTextValue();
const url = richText.getLinkUrl();
const text = richText.getText();
// Writing
const richText = SpreadsheetApp.newRichTextValue()
.setText(text)
.setLinkUrl(url)
.build();
range.setRichTextValue(richText);
Implementation Plan
Phase 1: Add Native GAS Conversion Logic
File: src/ScheduleAdapter.js
Replace Fiddler calls with native GAS:
_loadData() {
const sheet = this._getSheet();
const lastRow = sheet.getLastRow();
const lastCol = sheet.getLastColumn();
// Get headers
const headers = sheet.getRange(1, 1, 1, lastCol).getValues()[0];
// Get values
const values = sheet.getRange(2, 1, lastRow - 1, lastCol).getValues();
// Get RichText for Route and Ride columns
const routeColIndex = headers.indexOf('Route');
const rideColIndex = headers.indexOf('Ride');
const richTextValues = sheet.getRange(2, 1, lastRow - 1, lastCol).getRichTextValues();
// Convert to objects
this._data = values.map((row, i) => {
const obj = { _rowNum: i + 2 };
headers.forEach((header, j) => {
if (j === routeColIndex || j === rideColIndex) {
// Extract URL from RichText
const richText = richTextValues[i][j];
obj[header] = richText ? richText.getLinkUrl() || row[j] : row[j];
} else {
obj[header] = row[j];
}
});
return obj;
});
}
Phase 2: Update Save Logic for RichText
File: src/ScheduleAdapter.js
Replace formula preservation with RichText writes:
save() {
const sheet = this._getSheet();
const dirtyRows = this._data.filter(row => row._dirtyFields?.size > 0);
dirtyRows.forEach(row => {
row._dirtyFields.forEach(fieldName => {
const col = this._headers.indexOf(fieldName) + 1;
const cell = sheet.getRange(row._rowNum, col);
// Special handling for Route/Ride columns
if (fieldName === 'Route' || fieldName === 'Ride') {
const { text, url } = this._parseRouteOrRide(row[fieldName]);
const richText = SpreadsheetApp.newRichTextValue()
.setText(text)
.setLinkUrl(url)
.build();
cell.setRichTextValue(richText);
} else if (typeof row[fieldName] === 'string' && row[fieldName].startsWith('=')) {
cell.setFormula(row[fieldName]);
} else {
cell.setValue(row[fieldName]);
}
});
row._dirtyFields.clear();
});
SpreadsheetApp.flush();
}
Phase 3: Remove Formula Complexity
Files to Delete/Modify:
- ✅ Delete:
src/HyperlinkUtils.js (entire file, ~100+ lines)
- ✅ Delete:
test/__tests__/HyperlinkUtils.test.js
- ✅ Remove from
src/ScheduleAdapter.js:
_storeFormulas() method
_overlayFormulas() method
- PropertiesService formula storage logic
- ✅ Remove from
src/Exports.js: HyperlinkUtils export
- ✅ Remove from
src/gas-globals.d.ts: HyperlinkUtils declaration
- ✅ Update
package.json: Remove bmPreFiddler dependency
Phase 4: Update Row Class
File: src/RowCore.js
Simplify Route/Ride accessors (no formula parsing needed):
get RouteURL() {
// Before: return HyperlinkUtils.parseHyperlinkFormula(this._data.Route).url;
// After: return this._data.Route; // Already the URL from RichText
}
setRouteLink(text, url) {
// Before: this._data.Route = `=HYPERLINK("${url}", "${text}")`;
// After: Store both for ScheduleAdapter to create RichText
this._data.Route = { text, url };
this._markDirty('Route');
}
Testing Requirements
Unit Tests
- ✅ Native GAS array ↔ object conversion
- ✅ RichText URL extraction
- ✅ RichText creation and writing
- ✅ Row.RouteURL getter returns correct URL
- ✅ Row.setRouteLink marks field dirty
Integration Tests
Create test/__tests__/ScheduleAdapter.richtext.test.js:
- Load data with RichText hyperlinks
- Extract URLs correctly
- Save RichText hyperlinks
- Verify spreadsheet contains correct RichText
Manual Testing in GAS
- Deploy to dev environment
- Open spreadsheet with existing HYPERLINK formulas
- Run menu command that reads Route/Ride URLs
- Verify URLs extracted correctly
- Run menu command that writes Route/Ride links
- Verify spreadsheet shows RichText hyperlinks (not formulas)
- Click links to verify they work
Benefits
✅ Remove ~100-150 lines of code
✅ Remove external dependency (Fiddler/bmPreFiddler)
✅ Simpler mental model (RichText vs formula strings)
✅ Native GAS feature (better long-term support)
✅ No PropertiesService storage (one less state management concern)
✅ Better performance (no formula parsing overhead)
Migration Notes
Backward Compatibility:
- Existing spreadsheets with HYPERLINK formulas will need one-time conversion
- Create migration script:
scripts/migrate-formulas-to-richtext.js
- Migration reads formulas, converts to RichText, writes back
Rollout Strategy:
- Deploy code with both formula + RichText support
- Run migration script on production spreadsheet
- Verify all links work
- Remove formula support code
Related Issues
- Addresses complexity mentioned in ScheduleAdapter architecture discussions
- Enables further simplification of Row abstraction
- Reduces technical debt from formula preservation logic
Motivation
The current architecture uses Fiddler (bmPreFiddler) for spreadsheet I/O and stores hyperlinks as HYPERLINK formulas. This creates significant complexity:
=HYPERLINK("url", "text")constructionKey Insight: RichText objects provide native hyperlink support in GAS, making formula complexity unnecessary. However, Fiddler doesn't support RichText, so replacing formulas with RichText requires removing Fiddler.
Net Benefit: Remove ~100-150 lines of code + external dependency
Current Implementation
With Fiddler + Formulas:
Proposed Implementation
With Native GAS + RichText:
Implementation Plan
Phase 1: Add Native GAS Conversion Logic
File:
src/ScheduleAdapter.jsReplace Fiddler calls with native GAS:
Phase 2: Update Save Logic for RichText
File:
src/ScheduleAdapter.jsReplace formula preservation with RichText writes:
Phase 3: Remove Formula Complexity
Files to Delete/Modify:
src/HyperlinkUtils.js(entire file, ~100+ lines)test/__tests__/HyperlinkUtils.test.jssrc/ScheduleAdapter.js:_storeFormulas()method_overlayFormulas()methodsrc/Exports.js: HyperlinkUtils exportsrc/gas-globals.d.ts: HyperlinkUtils declarationpackage.json: Remove bmPreFiddler dependencyPhase 4: Update Row Class
File:
src/RowCore.jsSimplify Route/Ride accessors (no formula parsing needed):
Testing Requirements
Unit Tests
Integration Tests
Create
test/__tests__/ScheduleAdapter.richtext.test.js:Manual Testing in GAS
Benefits
✅ Remove ~100-150 lines of code
✅ Remove external dependency (Fiddler/bmPreFiddler)
✅ Simpler mental model (RichText vs formula strings)
✅ Native GAS feature (better long-term support)
✅ No PropertiesService storage (one less state management concern)
✅ Better performance (no formula parsing overhead)
Migration Notes
Backward Compatibility:
scripts/migrate-formulas-to-richtext.jsRollout Strategy:
Related Issues