@jacobknightley/fabric-format 0.0.3 → 0.0.5

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
@@ -50,13 +50,11 @@ Format Fabric notebooks directly in your browser with a single click.
50
50
 
51
51
  1. Download `fabric-format-chromium.zip` from the [latest release](https://github.com/jacobknightley/fabric-format/releases)
52
52
  2. Extract the zip file
53
- 3. Open your browser's extension page:
54
- - **Chrome:** `chrome://extensions`
55
- - **Edge:** `edge://extensions`
56
- 4. Enable **Developer mode** (toggle in top-right)
57
- 5. Click **Load unpacked** and select the extracted folder
53
+ 3. Load the unpacked extension in your browser:
54
+ - **Chrome:** [Install an unpacked extension](https://developer.chrome.com/docs/extensions/get-started/tutorial/hello-world#load-unpacked)
55
+ - **Edge:** [Sideload an extension](https://learn.microsoft.com/en-us/microsoft-edge/extensions-chromium/getting-started/extension-sideloading)
58
56
 
59
- > **Note:** Plan to eventually support directly in Chrome and Edge extension stores.
57
+ > **Note:** Plan to eventually publish to the Chrome Web Store and Edge Add-ons.
60
58
 
61
59
  ### Usage
62
60
 
package/dist/index.d.ts CHANGED
@@ -12,7 +12,7 @@
12
12
  export { formatSql, needsFormatting } from './formatters/sparksql/index.js';
13
13
  export { getFormatterRegistry, detectLanguage, SqlFormatter, getSqlFormatter, isSqlCode, PythonFormatter, getPythonFormatter, isPythonCode, type LanguageFormatter, type FormatterOptions, type FormatResult, type FormatterConfig, type FormatterRegistry, } from './formatters/index.js';
14
14
  export { formatCell, formatCellSync, initializePythonFormatter, isPythonFormatterReady, type FormatCellResult, type CellType, } from './cell-formatter.js';
15
- export { parseNotebook, formatNotebook, type NotebookCell, type FabricNotebook, type FormatStats, } from './notebook-formatter.js';
15
+ export { parseNotebook, formatNotebook, NotebookStructureError, type NotebookCell, type FabricNotebook, type FormatStats, } from './notebook-formatter.js';
16
16
  export { DEFAULT_RUFF_CONFIG, RUFF_WASM_CONFIG, type RuffConfig, type RuffFormatConfig, type WasmInitOptions, } from './formatters/python/index.js';
17
17
  export { hasFormatOff, detectCollapseDirectives, hasCollapseDirective, type FormatDirectiveInfo } from './formatters/sparksql/index.js';
18
18
  export type { AnalyzerResult, FormattingState, MultiArgFunctionInfo, WindowDefInfo, TokenContext, PendingComment, ExpandedFunction, ExpandedWindow } from './formatters/sparksql/types.js';
package/dist/index.js CHANGED
@@ -30,7 +30,7 @@ export { formatCell, formatCellSync, initializePythonFormatter, isPythonFormatte
30
30
  // ============================================================================
31
31
  // Notebook Formatter (High-level API)
32
32
  // ============================================================================
33
- export { parseNotebook, formatNotebook, } from './notebook-formatter.js';
33
+ export { parseNotebook, formatNotebook, NotebookStructureError, } from './notebook-formatter.js';
34
34
  // ============================================================================
35
35
  // Configuration (Python/Ruff)
36
36
  // ============================================================================
@@ -79,6 +79,14 @@ export interface FormatStats {
79
79
  cellsSkipped: number;
80
80
  errors: string[];
81
81
  }
82
+ /** Error thrown when a notebook cell has invalid structure */
83
+ export declare class NotebookStructureError extends Error {
84
+ readonly cellIndex: number;
85
+ readonly lineNumber: number;
86
+ readonly metadataLanguage: string;
87
+ readonly fileDefaultLanguage: string;
88
+ constructor(message: string, cellIndex: number, lineNumber: number, metadataLanguage: string, fileDefaultLanguage: string);
89
+ }
82
90
  /**
83
91
  * Parse a Fabric notebook file into cells.
84
92
  * @param content The file content
@@ -66,12 +66,68 @@ const R_CONFIG = {
66
66
  fabricHeader: '# Fabric notebook source', // Check if R has different header
67
67
  defaultLanguage: 'r',
68
68
  };
69
+ /** Error thrown when a notebook cell has invalid structure */
70
+ export class NotebookStructureError extends Error {
71
+ cellIndex;
72
+ lineNumber;
73
+ metadataLanguage;
74
+ fileDefaultLanguage;
75
+ constructor(message, cellIndex, lineNumber, metadataLanguage, fileDefaultLanguage) {
76
+ super(message);
77
+ this.cellIndex = cellIndex;
78
+ this.lineNumber = lineNumber;
79
+ this.metadataLanguage = metadataLanguage;
80
+ this.fileDefaultLanguage = fileDefaultLanguage;
81
+ this.name = 'NotebookStructureError';
82
+ }
83
+ }
69
84
  // ============================================================================
70
85
  // INTERNAL UTILITIES
71
86
  // ============================================================================
72
- // ============================================================================
73
- // INTERNAL UTILITIES
74
- // ============================================================================
87
+ /**
88
+ * Get the valid raw (uncommented) languages for a file type.
89
+ * Raw cells must match the file's native language - other languages must use MAGIC prefix.
90
+ */
91
+ function getValidRawLanguages(defaultLanguage) {
92
+ switch (defaultLanguage) {
93
+ case 'python':
94
+ // Python files: raw cells can be python or pyspark
95
+ return new Set(['python', 'pyspark']);
96
+ case 'sparksql':
97
+ // SQL files: raw cells must be sparksql
98
+ return new Set(['sparksql']);
99
+ case 'scala':
100
+ // Scala files: raw cells must be scala
101
+ return new Set(['scala']);
102
+ case 'r':
103
+ // R files: raw cells must be r
104
+ return new Set(['r']);
105
+ default:
106
+ return new Set([defaultLanguage]);
107
+ }
108
+ }
109
+ /**
110
+ * Validate that a raw cell's metadata language matches the file type.
111
+ * Throws NotebookStructureError if there's a mismatch.
112
+ */
113
+ function validateRawCellLanguage(metadataLanguage, isRawCell, config, cellIndex, lineNumber) {
114
+ if (!isRawCell || !metadataLanguage) {
115
+ // MAGIC cells can have any language, and cells without metadata are fine
116
+ return;
117
+ }
118
+ const validRawLanguages = getValidRawLanguages(config.defaultLanguage);
119
+ const normalizedLanguage = metadataLanguage.toLowerCase();
120
+ if (!validRawLanguages.has(normalizedLanguage)) {
121
+ const fileType = config.defaultLanguage === 'python' ? '.py' :
122
+ config.defaultLanguage === 'sparksql' ? '.sql' :
123
+ config.defaultLanguage === 'scala' ? '.scala' :
124
+ config.defaultLanguage === 'r' ? '.r' :
125
+ `.${config.defaultLanguage}`;
126
+ throw new NotebookStructureError(`Invalid notebook structure: Cell ${cellIndex + 1} (line ${lineNumber + 1}) has metadata language "${metadataLanguage}" ` +
127
+ `but is not wrapped with MAGIC prefix. In ${fileType} files, only ${config.defaultLanguage} cells can be raw/uncommented. ` +
128
+ `Other languages (like ${metadataLanguage}) must use the "${config.magicPrefix}%%" prefix.`, cellIndex, lineNumber, metadataLanguage, config.defaultLanguage);
129
+ }
130
+ }
75
131
  /**
76
132
  * Line ending constant - this library standardizes on LF.
77
133
  */
@@ -291,6 +347,9 @@ export function parseNotebook(content, fileExtension) {
291
347
  const magicCommand = extractMagicCommand(originalLines, config);
292
348
  // Check if it's a MAGIC cell
293
349
  const isMagicCell = originalLines.some(l => l.trim().startsWith(config.magicPrefix.trim()));
350
+ // Validate that raw cells have a language matching the file type
351
+ const cellIndex = result.cells.length;
352
+ validateRawCellLanguage(metadataLanguage, !isMagicCell, config, cellIndex, j);
294
353
  // Extract content
295
354
  let content;
296
355
  let contentStartLine = j;
@@ -333,15 +392,27 @@ export function parseNotebook(content, fileExtension) {
333
392
  function replaceCell(fileContent, cell, formattedContent, config) {
334
393
  const lines = fileContent.split(/\r?\n/);
335
394
  let newLines;
336
- if (cell.isMagicCell) {
337
- newLines = addMagicPrefix(formattedContent, config);
395
+ if (cell.isMagicCell && cell.magicCommand) {
396
+ // For magic cells, prepend the magic command (without trailing whitespace)
397
+ const magicCommandLine = config.magicPrefix + '%%' + cell.magicCommand;
398
+ newLines = [magicCommandLine, ...addMagicPrefix(formattedContent, config)];
399
+ // Find where the magic command line starts (search backwards from contentStartLine)
400
+ let magicLineIndex = cell.contentStartLine - 1;
401
+ while (magicLineIndex >= 0 && !lines[magicLineIndex].trim().startsWith(config.magicPrefix + '%%')) {
402
+ magicLineIndex--;
403
+ }
404
+ const before = lines.slice(0, magicLineIndex >= 0 ? magicLineIndex : cell.contentStartLine);
405
+ const after = lines.slice(cell.contentEndLine + 1);
406
+ return [...before, ...newLines, ...after].join(LINE_ENDING);
338
407
  }
339
408
  else {
340
- newLines = formattedContent.split(/\r?\n/);
409
+ newLines = cell.isMagicCell
410
+ ? addMagicPrefix(formattedContent, config)
411
+ : formattedContent.split(/\r?\n/);
412
+ const before = lines.slice(0, cell.contentStartLine);
413
+ const after = lines.slice(cell.contentEndLine + 1);
414
+ return [...before, ...newLines, ...after].join(LINE_ENDING);
341
415
  }
342
- const before = lines.slice(0, cell.contentStartLine);
343
- const after = lines.slice(cell.contentEndLine + 1);
344
- return [...before, ...newLines, ...after].join(LINE_ENDING);
345
416
  }
346
417
  /**
347
418
  * Format all cells in a Fabric notebook.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jacobknightley/fabric-format",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "type": "module",
5
5
  "description": "A fast, opinionated formatter for Microsoft Fabric notebooks with Spark SQL and Python support",
6
6
  "main": "dist/index.js",