Przeglądaj źródła

Spreadsheet formulas can be styled with certain syntaxes

main
Rocketsoup 1 rok temu
rodzic
commit
03ffef5f19
7 zmienionych plików z 293 dodań i 55 usunięć
  1. 71
    1
      js/markdown.js
  2. 1
    1
      js/markdown.min.js
  3. 65
    23
      js/spreadsheet.js
  4. 1
    1
      js/spreadsheet.min.js
  5. 77
    2
      php/markdown.php
  6. 65
    26
      php/spreadsheet.php
  7. 13
    1
      spreadsheet.md

+ 71
- 1
js/markdown.js Wyświetl plik

@@ -2317,6 +2317,14 @@ class MDHorizontalRuleReader extends MDReader {
2317 2317
  */
2318 2318
 class MDTableReader extends MDReader {
2319 2319
 	/**
2320
+	 * If cell contents begin with `=`, treat entire contents as plaintext.
2321
+	 * Used by spreadsheet add-on to prevent equation operators from being
2322
+	 * interpreted as markdown.
2323
+	 * @type {boolean}
2324
+	 */
2325
+	preferFormulas = false;
2326
+
2327
+	/**
2320 2328
 	 * @param {MDState} state
2321 2329
 	 * @param {boolean} isHeader
2322 2330
 	 * @return {MDTableRowNode|null}
@@ -2329,8 +2337,18 @@ class MDTableReader extends MDReader {
2329 2337
 		if (line.startsWith('|')) line = line.substring(1);
2330 2338
 		if (line.endsWith('|')) line = line.substring(0, line.length - 1);
2331 2339
 		let cellTokens = line.split('|');
2340
+		const reader = this;
2332 2341
 		let cells = cellTokens.map(function(token) {
2333
-			let content = state.inlineMarkdownToNode(token.trim());
2342
+			const trimmedToken = token.trim();
2343
+			let content;
2344
+			if (reader.preferFormulas && trimmedToken.indexOf('=') >= 0) {
2345
+				content = reader.#preserveFormula(state, trimmedToken);
2346
+				if (content == null) {
2347
+					content = state.inlineMarkdownToNode(trimmedToken);
2348
+				}
2349
+			} else {
2350
+				content = state.inlineMarkdownToNode(token.trim());
2351
+			}
2334 2352
 			return isHeader ? new MDTableHeaderCellNode(content) : new MDTableCellNode(content);
2335 2353
 		});
2336 2354
 		state.p = p;
@@ -2338,6 +2356,44 @@ class MDTableReader extends MDReader {
2338 2356
 	}
2339 2357
 
2340 2358
 	/**
2359
+	 * @param {MDState} state
2360
+	 * @param {string} cellContents
2361
+	 * @returns {MDNode|null}
2362
+	 */
2363
+	#preserveFormula(state, cellContents) {
2364
+		// Up to three prefix punctuation patterns, formula, then three matching
2365
+		// suffixes. Not guaranteed to catch every possible syntax but an awful lot.
2366
+		const groups = /^([^a-z0-9\s]*)([^a-z0-9\s]*)([^a-z0-9\s]*)(=.*)\3\2\1$/i.exec(cellContents);
2367
+		if (groups === null) return null;
2368
+		const prefix = groups[1] + groups[2] + groups[3];
2369
+		const formula = groups[4];
2370
+		if (prefix.length == 0) {
2371
+			return new MDTextNode(formula);
2372
+		}
2373
+		const suffix = groups[3] + groups[2] + groups[1];
2374
+		// Parse substitute markdown with the same prefix and suffix but just
2375
+		// an "x" as content. We'll swap in the unaltered formula into the
2376
+		// parsed nodes.
2377
+		const tempInline = prefix + 'x' + suffix;
2378
+		const tempNodes = state.inlineMarkdownToNodes(tempInline);
2379
+		if (tempNodes.length != 1) return null;
2380
+		var foundText = false;
2381
+		if (tempNodes[0] instanceof MDTextNode && tempNodes[0].text === 'x') {
2382
+			tempNodes[0].text = formula;
2383
+			foundText = true;
2384
+		} else {
2385
+			tempNodes[0].visitChildren(function(node) {
2386
+				if (node instanceof MDTextNode && node.text === 'x') {
2387
+					node.text = formula;
2388
+					foundText = true;
2389
+				}
2390
+			});
2391
+		}
2392
+		if (!foundText) return null;
2393
+		return tempNodes[0];
2394
+	}
2395
+
2396
+	/**
2341 2397
 	 * @param {string} line
2342 2398
 	 * @returns {string[]}
2343 2399
 	 */
@@ -3604,6 +3660,20 @@ class MDTableNode extends MDBlockNode {
3604 3660
 		this.#bodyRows = bodyRows;
3605 3661
 	}
3606 3662
 
3663
+	/**
3664
+	 * Returns a given body cell.
3665
+	 *
3666
+	 * @param {number} column
3667
+	 * @param {number} row
3668
+	 * @returns {MDTableCellNode|null} cell or `null` if out of bounds
3669
+	 */
3670
+	bodyCellAt(column, row) {
3671
+		const rowNode = this.bodyRows[row];
3672
+		if (rowNode === undefined) return null;
3673
+		const cellNode = rowNode.children[column];
3674
+		return (cellNode === undefined) ? null : cellNode;
3675
+	}
3676
+
3607 3677
 	#recalculateChildren() {
3608 3678
 		this.children = [ this.#headerRow, ...this.#bodyRows ];
3609 3679
 	}

+ 1
- 1
js/markdown.min.js
Plik diff jest za duży
Wyświetl plik


+ 65
- 23
js/spreadsheet.js Wyświetl plik

@@ -2953,6 +2953,14 @@ class SpreadsheetCell {
2953 2953
  * `MDTableReader`. Tables without at least one formula will not be altered.
2954 2954
  */
2955 2955
 class MDSpreadsheetReader extends MDReader {
2956
+	preProcess(state) {
2957
+		for (const reader of state.readersByBlockPriority) {
2958
+			if (reader instanceof MDTableReader) {
2959
+				reader.preferFormulas = true;
2960
+			}
2961
+		}
2962
+	}
2963
+
2956 2964
 	postProcess(state, nodes) {
2957 2965
 		for (const node of nodes) {
2958 2966
 			if (node instanceof MDTableNode) {
@@ -2977,8 +2985,8 @@ class MDSpreadsheetReader extends MDReader {
2977 2985
 		const grid = new SpreadsheetGrid(columnCount, rowCount);
2978 2986
 		for (var c = 0; c < columnCount; c++) {
2979 2987
 			for (var r = 0; r < rowCount; r++) {
2980
-				const cellNode = tableNode.bodyRows[r].children[c];
2981
-				if (cellNode === undefined) continue;
2988
+				const cellNode = tableNode.bodyCellAt(c, r);
2989
+				if (cellNode === null) continue;
2982 2990
 				const cellText = cellNode.toPlaintext(state);
2983 2991
 				const gridCell = grid.cells[c][r];
2984 2992
 				gridCell.originalValue = CellValue.fromCellString(cellText);
@@ -2993,7 +3001,7 @@ class MDSpreadsheetReader extends MDReader {
2993 3001
 		var isCalculated = false;
2994 3002
 		for (var c = 0; c < columnCount && !isCalculated; c++) {
2995 3003
 			for (var r = 0; r < rowCount; r++) {
2996
-				if (grid.cells[c][r].isCalculated) {
3004
+				if (grid.cellAt(new CellAddress(c, r)).isCalculated) {
2997 3005
 					isCalculated = true;
2998 3006
 					break;
2999 3007
 				}
@@ -3004,28 +3012,62 @@ class MDSpreadsheetReader extends MDReader {
3004 3012
 		// Copy results back to table
3005 3013
 		for (var c = 0; c < columnCount; c++) {
3006 3014
 			for (var r = 0; r < rowCount; r++) {
3007
-				const cellNode = tableNode.bodyRows[r].children[c];
3008
-				if (cellNode === undefined) continue;
3009
-				const gridCell = grid.cells[c][r];
3010
-				const gridValue = gridCell.outputValue;
3011
-				const cellText = gridValue.formattedValue;
3015
+				const cellNode = tableNode.bodyCellAt(c, r);
3016
+				const gridCell = grid.cellAt(new CellAddress(c, r));
3017
+				if (cellNode === null || gridCell === null) continue;
3018
+				this.#populateCell(cellNode, gridCell, state, c, r);
3019
+			}
3020
+		}
3021
+	}
3022
+
3023
+	/**
3024
+	 * @param {MDTableCellNode} cellNode
3025
+	 * @param {SpreadsheetCell} gridCell
3026
+	 * @param {MDState} state
3027
+	 * @param {number} c - column index
3028
+	 * @param {number} r - row index
3029
+	 */
3030
+	#populateCell(cellNode, gridCell, state, c, r) {
3031
+		const gridValue = gridCell.outputValue;
3032
+		if (gridValue === null) return;
3033
+		const oldCellText = cellNode.toPlaintext(state).trim();
3034
+		const cellText = gridValue.formattedValue;
3035
+		if (cellText != oldCellText) {
3036
+			// Try to insert the text into any nested whole-value formatting nodes
3037
+			// if possible
3038
+			if (!this.#findTextNode(cellNode, oldCellText, cellText)) {
3039
+				// Contents contain mixed formatting. We'll have to just replace
3040
+				// the whole thing.
3012 3041
 				cellNode.children = [ new MDTextNode(cellText) ];
3013
-				if (gridCell.isCalculated) {
3014
-					cellNode.cssClasses.push('calculated');
3015
-				}
3016
-				cellNode.cssClasses.push(`spreadsheet-type-${gridValue.type}`);
3017
-				if (gridValue.type == CellValue.TYPE_ERROR) {
3018
-					cellNode.attributes['title'] = gridValue.value;
3019
-				}
3020
-				const gridNumber = gridValue.numericValue();
3021
-				if (gridNumber !== null) {
3022
-					cellNode.attributes['data-numeric-value'] = `${gridNumber}`;
3023
-				}
3024
-				const gridString = gridValue.stringValue(false);
3025
-				if (gridString !== null) {
3026
-					cellNode.attributes['data-string-value'] = gridString;
3027
-				}
3028 3042
 			}
3029 3043
 		}
3044
+		if (gridCell.isCalculated) {
3045
+			cellNode.cssClasses.push('calculated');
3046
+		}
3047
+		cellNode.cssClasses.push(`spreadsheet-type-${gridValue.type}`);
3048
+		if (gridValue.type == CellValue.TYPE_ERROR) {
3049
+			cellNode.attributes['title'] = gridValue.value;
3050
+		}
3051
+		const gridNumber = gridValue.numericValue();
3052
+		if (gridNumber !== null) {
3053
+			cellNode.attributes['data-numeric-value'] = `${gridNumber}`;
3054
+		}
3055
+		const gridString = gridValue.stringValue(false);
3056
+		if (gridString !== null) {
3057
+			cellNode.attributes['data-string-value'] = gridString;
3058
+		}
3059
+	}
3060
+
3061
+	#findTextNode(startNode, expectedText, newText) {
3062
+		if (startNode instanceof MDTextNode) {
3063
+			if (startNode.text.trim() === expectedText.trim()) {
3064
+				startNode.text = newText;
3065
+				return true;
3066
+			}
3067
+		}
3068
+		for (const child of startNode.children) {
3069
+			if (this.#findTextNode(child, expectedText, newText)) return true;
3070
+		}
3071
+		return false;
3030 3072
 	}
3031 3073
 }

+ 1
- 1
js/spreadsheet.min.js
Plik diff jest za duży
Wyświetl plik


+ 77
- 2
php/markdown.php Wyświetl plik

@@ -2079,6 +2079,14 @@ class MDHorizontalRuleReader extends MDReader {
2079 2079
  * Supports `MDTagModifier` suffix.
2080 2080
  */
2081 2081
 class MDTableReader extends MDReader {
2082
+	/**
2083
+	 * If cell contents begin with `=`, treat entire contents as plaintext.
2084
+	 * Used by spreadsheet add-on to prevent equation operators from being
2085
+	 * interpreted as markdown.
2086
+	 * @type {boolean}
2087
+	 */
2088
+	public bool $preferFormulas = false;
2089
+
2082 2090
 	private function readTableRow(MDState $state, bool $isHeader): ?MDTableRowNode {
2083 2091
 		if (!$state->hasLines(1)) return null;
2084 2092
 		$p = $state->p;
@@ -2087,8 +2095,16 @@ class MDTableReader extends MDReader {
2087 2095
 		if (str_starts_with($line, '|')) $line = mb_substr($line, 1);
2088 2096
 		if (str_ends_with($line, '|')) $line = mb_substr($line, 0, mb_strlen($line) - 1);
2089 2097
 		$cellTokens = explode('|', $line);
2090
-		$cells = array_map(function($token) use ($isHeader, $state) {
2091
-			$content = $state->inlineMarkdownToNode(trim($token));
2098
+		$cells = array_map(function($token) use ($state, $isHeader) {
2099
+			$trimmedToken = trim($token);
2100
+			if ($this->preferFormulas && strpos($trimmedToken, '=') !== false) {
2101
+				$content = $this->preserveFormula($state, $trimmedToken);
2102
+				if ($content === null) {
2103
+					$content = $state->inlineMarkdownToNode($trimmedToken);
2104
+				}
2105
+			} else {
2106
+				$content = $state->inlineMarkdownToNode($trimmedToken);
2107
+			}
2092 2108
 			return $isHeader ? new MDTableHeaderCellNode($content) : new MDTableCellNode($content);
2093 2109
 		}, $cellTokens);
2094 2110
 		$state->p = $p;
@@ -2096,6 +2112,47 @@ class MDTableReader extends MDReader {
2096 2112
 	}
2097 2113
 
2098 2114
 	/**
2115
+	 * @param MDState $state
2116
+	 * @param string $cellContents
2117
+	 * @return ?MDNode
2118
+	 */
2119
+	private function preserveFormula(MDState $state, string $cellContents): ?MDNode {
2120
+		// Up to three prefix punctuation patterns, formula, then three matching
2121
+		// suffixes. Not guaranteed to catch every possible syntax but an awful lot.
2122
+		// Using preg_match instead for... reasons.
2123
+		$regex = '/^([^a-z0-9\\s]*)([^a-z0-9\\s]*)([^a-z0-9\\s]*)(=.*)\\3\\2\\1$/i';
2124
+		if (!preg_match($regex, $cellContents, $groups)) {
2125
+			return null;
2126
+		}
2127
+		$prefix = $groups[1] . $groups[2] . $groups[3];
2128
+		$formula = $groups[4];
2129
+		if ($prefix === '') {
2130
+			return new MDTextNode($formula);
2131
+		}
2132
+		$suffix = $groups[3] . $groups[2] . $groups[1];
2133
+		// Parse substitute markdown with the same prefix and suffix but just
2134
+		// an "x" as content. We'll swap in the unaltered formula into the
2135
+		// parsed nodes.
2136
+		$tempInline = $prefix . 'x' . $suffix;
2137
+		$tempNodes = $state->inlineMarkdownToNodes($tempInline);
2138
+		if (count($tempNodes) != 1) return null;
2139
+		$foundText = false;
2140
+		if ($tempNodes[0] instanceof MDTextNode && $tempNodes[0]->text === 'x') {
2141
+			$tempNodes[0]->text = $formula;
2142
+			$foundText = true;
2143
+		} else {
2144
+			$tempNodes[0]->visitChildren(function($node) use ($formula, &$foundText) {
2145
+				if ($node instanceof MDTextNode && $node->text === 'x') {
2146
+					$node->text = $formula;
2147
+					$foundText = true;
2148
+				}
2149
+			});
2150
+		}
2151
+		if (!$foundText) return null;
2152
+		return $tempNodes[0];
2153
+	}
2154
+
2155
+	/**
2099 2156
 	 * @param string $line
2100 2157
 	 * @return string[]
2101 2158
 	 */
@@ -3317,6 +3374,20 @@ class MDTableNode extends MDBlockNode {
3317 3374
 		parent::__construct(array_merge([ $headerRow ], $bodyRows));
3318 3375
 	}
3319 3376
 
3377
+	/**
3378
+	 * Returns a given body cell.
3379
+	 *
3380
+	 * @param {number} column
3381
+	 * @param {number} row
3382
+	 * @returns {MDTableCellNode|null} cell or `null` if out of bounds
3383
+	 */
3384
+	public function bodyCellAt(int $column, int $row): ?MDTableCellNode {
3385
+		$rowNode = $this->bodyRows()[$row] ?? null;
3386
+		if ($rowNode === null) return null;
3387
+		$cellNode = $rowNode->children[$column] ?? null;
3388
+		return ($cellNode === null) ? null : $cellNode;
3389
+	}
3390
+
3320 3391
 	public function applyAlignments() {
3321 3392
 		foreach ($this->children as $child) {
3322 3393
 			$this->applyAlignmentsToRow($child);
@@ -3490,6 +3561,10 @@ class MDTextNode extends MDInlineNode {
3490 3561
 	public function toPlaintext(MDState $state): string {
3491 3562
 		return $this->text;
3492 3563
 	}
3564
+
3565
+	public function __toString(): string {
3566
+		return "<MDTextNode \"{$this->text}\">";
3567
+	}
3493 3568
 }
3494 3569
 
3495 3570
 /**

+ 65
- 26
php/spreadsheet.php Wyświetl plik

@@ -2810,6 +2810,14 @@ class SpreadsheetCell {
2810 2810
  * `MDTableReader`. Tables without at least one formula will not be altered.
2811 2811
  */
2812 2812
 class MDSpreadsheetReader extends MDReader {
2813
+	public function preProcess(MDState $state) {
2814
+		foreach ($state->readersByBlockPriority as $reader) {
2815
+			if ($reader instanceof MDTableReader) {
2816
+				$reader->preferFormulas = true;
2817
+			}
2818
+		}
2819
+	}
2820
+
2813 2821
 	public function postProcess(MDState $state, array &$nodes) {
2814 2822
 		foreach ($nodes as $node) {
2815 2823
 			if ($node instanceof MDTableNode) {
@@ -2830,9 +2838,7 @@ class MDSpreadsheetReader extends MDReader {
2830 2838
 		$grid = new SpreadsheetGrid($columnCount, $rowCount);
2831 2839
 		for ($c = 0; $c < $columnCount; $c++) {
2832 2840
 			for ($r = 0; $r < $rowCount; $r++) {
2833
-				$row = $tableNode->bodyRows()[$r] ?? null;
2834
-				if ($row === null) continue;
2835
-				$cellNode = $row->children[$c] ?? null;
2841
+				$cellNode = $tableNode->bodyCellAt($c, $r);
2836 2842
 				if ($cellNode === null) continue;
2837 2843
 				$cellText = $cellNode->toPlaintext($state);
2838 2844
 				$gridCell = $grid->cells[$c][$r];
@@ -2848,7 +2854,7 @@ class MDSpreadsheetReader extends MDReader {
2848 2854
 		$isCalculated = false;
2849 2855
 		for ($c = 0; $c < $columnCount && !$isCalculated; $c++) {
2850 2856
 			for ($r = 0; $r < $rowCount; $r++) {
2851
-				if ($grid->cells[$c][$r]->isCalculated) {
2857
+				if ($grid->cellAt(new CellAddress($c, $r))->isCalculated) {
2852 2858
 					$isCalculated = true;
2853 2859
 					break;
2854 2860
 				}
@@ -2859,31 +2865,64 @@ class MDSpreadsheetReader extends MDReader {
2859 2865
 		// Copy results back to table
2860 2866
 		for ($c = 0; $c < $columnCount; $c++) {
2861 2867
 			for ($r = 0; $r < $rowCount; $r++) {
2862
-				$row = $tableNode->bodyRows()[$r] ?? null;
2863
-				if ($row === null) continue;
2864
-				$cellNode = $row->children[$c] ?? null;
2865
-				if ($cellNode === null) continue;
2866
-				$gridCell = $grid->cells[$c][$r];
2867
-				$gridValue = $gridCell->outputValue;
2868
-				$cellText = $gridValue->formattedValue;
2868
+				$cellNode = $tableNode->bodyCellAt($c, $r);
2869
+				$gridCell = $grid->cellAt(new CellAddress($c, $r));
2870
+				if ($cellNode === null || $gridCell === null) continue;
2871
+				$this->populateCell($cellNode, $gridCell, $state, $c, $r);
2872
+			}
2873
+		}
2874
+	}
2875
+
2876
+	/**
2877
+	 * @param MDTableCellNode $cellNode
2878
+	 * @param SpreadsheetCell $gridCell
2879
+	 * @param MDState $state
2880
+	 * @param int $c  column index
2881
+	 * @param int $r  row index
2882
+	 */
2883
+	private function populateCell(MDTableCellNode $cellNode,
2884
+			SpreadsheetCell $gridCell, MDState $state, int $c, int $r) {
2885
+		$gridValue = $gridCell->outputValue;
2886
+		if ($gridValue === null) return;
2887
+		$oldCellText = trim($cellNode->toPlaintext($state));
2888
+		$cellText = $gridValue->formattedValue;
2889
+		if ($cellText != $oldCellText) {
2890
+			// Try to insert the text into any nested whole-value formatting nodes
2891
+			// if possible
2892
+			if (!$this->findTextNode($cellNode, $oldCellText, $cellText)) {
2893
+				// Contents contain mixed formatting. We'll have to just replace
2894
+				// the whole thing.
2869 2895
 				$cellNode->children = [ new MDTextNode($cellText) ];
2870
-				if ($gridCell->isCalculated) {
2871
-					$cellNode->addClass('calculated');
2872
-				}
2873
-				$cellNode->addClass("spreadsheet-type-{$gridValue->type}");
2874
-				if ($gridValue->type === CellValue::TYPE_ERROR) {
2875
-					$cellNode->attributes['title'] = $gridValue->value;
2876
-				}
2877
-				$gridNumber = $gridValue->numericValue();
2878
-				if ($gridNumber !== null) {
2879
-					$cellNode->attributes['data-numeric-value'] = "{$gridNumber}";
2880
-				}
2881
-				$gridString = $gridValue->stringValue(false);
2882
-				if ($gridString !== null) {
2883
-					$cellNode->attributes['data-string-value'] = $gridString;
2884
-				}
2885 2896
 			}
2886 2897
 		}
2898
+		if ($gridCell->isCalculated) {
2899
+			$cellNode->addClass('calculated');
2900
+		}
2901
+		$cellNode->addClass("spreadsheet-type-{$gridValue->type}");
2902
+		if ($gridValue->type == CellValue::TYPE_ERROR) {
2903
+			$cellNode->attributes['title'] = $gridValue->value;
2904
+		}
2905
+		$gridNumber = $gridValue->numericValue();
2906
+		if ($gridNumber !== null) {
2907
+			$cellNode->attributes['data-numeric-value'] = "{$gridNumber}";
2908
+		}
2909
+		$gridString = $gridValue->stringValue(false);
2910
+		if ($gridString !== null) {
2911
+			$cellNode->attributes['data-string-value'] = $gridString;
2912
+		}
2913
+	}
2914
+
2915
+	private function findTextNode(MDNode $startNode, string $expectedText, string $newText): bool {
2916
+		if ($startNode instanceof MDTextNode) {
2917
+			if (trim($startNode->text) === trim($expectedText)) {
2918
+				$startNode->text = $newText;
2919
+				return true;
2920
+			}
2921
+		}
2922
+		foreach ($startNode->children as $child) {
2923
+			if ($this->findTextNode($child, $expectedText, $newText)) return true;
2924
+		}
2925
+		return false;
2887 2926
 	}
2888 2927
 }
2889 2928
 ?>

+ 13
- 1
spreadsheet.md Wyświetl plik

@@ -1,6 +1,6 @@
1 1
 # Spreadsheet Expressions
2 2
 
3
-Spreadsheet expressions are an optional feature not enabled in the default parser configurations. To enable it, include `spreadsheet.js` and create a `Markdown` instance with a `MDSpreadsheetReader`.
3
+Spreadsheet expressions are an optional feature not enabled in the default parser configurations. To enable it, include `spreadsheet.js` / `spreadsheet.php` and create a `Markdown` instance with a `MDSpreadsheetReader`.
4 4
 
5 5
 ## Expressions
6 6
 
@@ -94,3 +94,15 @@ To force a value to behave like text, prefix it with a `'`. E.g. `'0001` will be
94 94
 ## Errors
95 95
 
96 96
 If an expression cannot be evaluated, the cell will show an error symbol, such as `#REF`, `#SYNTAX`, or `#ERROR`. A more detailed message is in the `title` attribute and can be seen in a tooltip by mousing over it.
97
+
98
+## Styling
99
+
100
+The parser tries to be smart about differentiating operators from markdown syntax,
101
+but if an operator character (such as `*`) is mistaken for formatting, escape
102
+it with `\\*`.
103
+
104
+A limited set of styling can be applied to formula results if it consists of
105
+matching punctuation before and after. For example, `**=SUM(A:A)**` will
106
+render a sum in bold. Up to three such formats can be combined, e.g.
107
+`==~~__=SUM(A:A)__~~==`. Blank cells that are autofilled by a formula in the
108
+column cannot currently be styled in this way.

Ładowanie…
Anuluj
Zapisz