|
| 1 | +// Copyright 2024 Google Inc. Use of this source code is governed by an |
| 2 | +// MIT-style license that can be found in the LICENSE file or at |
| 3 | +// https://opensource.org/licenses/MIT. |
| 4 | + |
| 5 | +import * as postcss from 'postcss'; |
| 6 | +import type {CommentRaws} from 'postcss/lib/comment'; |
| 7 | + |
| 8 | +import {LazySource} from '../lazy-source'; |
| 9 | +import type * as sassInternal from '../sass-internal'; |
| 10 | +import {Interpolation} from '../interpolation'; |
| 11 | +import * as utils from '../utils'; |
| 12 | +import {ContainerProps, Statement, StatementWithChildren} from '.'; |
| 13 | +import {_Comment} from './comment-internal'; |
| 14 | +import {interceptIsClean} from './intercept-is-clean'; |
| 15 | +import * as sassParser from '../..'; |
| 16 | + |
| 17 | +/** |
| 18 | + * The set of raws supported by {@link SassComment}. |
| 19 | + * |
| 20 | + * @category Statement |
| 21 | + */ |
| 22 | +export interface SassCommentRaws extends Omit<CommentRaws, 'right'> { |
| 23 | + /** |
| 24 | + * Unlike PostCSS's, `CommentRaws.before`, this is added before `//` for |
| 25 | + * _every_ line of this comment. If any lines have more indentation than this, |
| 26 | + * it appears in {@link beforeLines} instead. |
| 27 | + */ |
| 28 | + before?: string; |
| 29 | + |
| 30 | + /** |
| 31 | + * For each line in the comment, this is the whitespace that appears before |
| 32 | + * the `//` _in addition to_ {@link before}. |
| 33 | + */ |
| 34 | + beforeLines?: string[]; |
| 35 | + |
| 36 | + /** |
| 37 | + * Unlike PostCSS's `CommentRaws.left`, this is added after `//` for _every_ |
| 38 | + * line in the comment that's not only whitespace. If any lines have more |
| 39 | + * initial whitespace than this, it appears in {@link SassComment.text} |
| 40 | + * instead. |
| 41 | + * |
| 42 | + * Lines that are only whitespace do not have `left` added to them, and |
| 43 | + * instead have all their whitespace directly in {@link SassComment.text}. |
| 44 | + */ |
| 45 | + left?: string; |
| 46 | +} |
| 47 | + |
| 48 | +/** |
| 49 | + * The subset of {@link SassCommentProps} that can be used to construct it |
| 50 | + * implicitly without calling `new SassComment()`. |
| 51 | + * |
| 52 | + * @category Statement |
| 53 | + */ |
| 54 | +export type SassCommentChildProps = ContainerProps & { |
| 55 | + raws?: SassCommentRaws; |
| 56 | + silentText: string; |
| 57 | +}; |
| 58 | + |
| 59 | +/** |
| 60 | + * The initializer properties for {@link SassComment}. |
| 61 | + * |
| 62 | + * @category Statement |
| 63 | + */ |
| 64 | +export type SassCommentProps = ContainerProps & { |
| 65 | + raws?: SassCommentRaws; |
| 66 | +} & ( |
| 67 | + | { |
| 68 | + silentText: string; |
| 69 | + } |
| 70 | + | {text: string} |
| 71 | + ); |
| 72 | + |
| 73 | +/** |
| 74 | + * A Sass-style "silent" comment. Extends [`postcss.Comment`]. |
| 75 | + * |
| 76 | + * [`postcss.Comment`]: https://postcss.org/api/#comment |
| 77 | + * |
| 78 | + * @category Statement |
| 79 | + */ |
| 80 | +export class SassComment |
| 81 | + extends _Comment<Partial<SassCommentProps>> |
| 82 | + implements Statement |
| 83 | +{ |
| 84 | + readonly sassType = 'sass-comment' as const; |
| 85 | + declare parent: StatementWithChildren | undefined; |
| 86 | + declare raws: SassCommentRaws; |
| 87 | + |
| 88 | + /** |
| 89 | + * The text of this comment, potentially spanning multiple lines. |
| 90 | + * |
| 91 | + * This is always the same as {@link text}, it just has a different name to |
| 92 | + * distinguish {@link SassCommentProps} from {@link CssCommentProps}. |
| 93 | + */ |
| 94 | + declare silentText: string; |
| 95 | + |
| 96 | + get text(): string { |
| 97 | + return this.silentText; |
| 98 | + } |
| 99 | + set text(value: string) { |
| 100 | + this.silentText = value; |
| 101 | + } |
| 102 | + |
| 103 | + constructor(defaults: SassCommentProps); |
| 104 | + /** @hidden */ |
| 105 | + constructor(_: undefined, inner: sassInternal.SilentComment); |
| 106 | + constructor(defaults?: SassCommentProps, inner?: sassInternal.SilentComment) { |
| 107 | + super(defaults as unknown as postcss.CommentProps); |
| 108 | + |
| 109 | + if (inner) { |
| 110 | + this.source = new LazySource(inner); |
| 111 | + |
| 112 | + const lineInfo = inner.text |
| 113 | + .trimRight() |
| 114 | + .split('\n') |
| 115 | + .map(line => { |
| 116 | + const index = line.indexOf('//'); |
| 117 | + const before = line.substring(0, index); |
| 118 | + const regexp = /[^ \t]/g; |
| 119 | + regexp.lastIndex = index + 2; |
| 120 | + const firstNonWhitespace = regexp.exec(line)?.index; |
| 121 | + if (firstNonWhitespace === undefined) { |
| 122 | + return {before, left: null, text: line.substring(index + 2)}; |
| 123 | + } |
| 124 | + |
| 125 | + const left = line.substring(index + 2, firstNonWhitespace); |
| 126 | + const text = line.substring(firstNonWhitespace); |
| 127 | + return {before, left, text}; |
| 128 | + }); |
| 129 | + |
| 130 | + // Dart Sass doesn't include the whitespace before the first `//` in |
| 131 | + // SilentComment.text, so we grab it directly from the SourceFile. |
| 132 | + let i = inner.span.start.offset - 1; |
| 133 | + for (; i >= 0; i--) { |
| 134 | + const char = inner.span.file.codeUnits[i]; |
| 135 | + if (char !== 0x20 && char !== 0x09) break; |
| 136 | + } |
| 137 | + lineInfo[0].before = inner.span.file.getText( |
| 138 | + i + 1, |
| 139 | + inner.span.start.offset |
| 140 | + ); |
| 141 | + |
| 142 | + const before = (this.raws.before = utils.longestCommonInitialSubstring( |
| 143 | + lineInfo.map(info => info.before) |
| 144 | + )); |
| 145 | + this.raws.beforeLines = lineInfo.map(info => |
| 146 | + info.before.substring(before.length) |
| 147 | + ); |
| 148 | + const left = (this.raws.left = utils.longestCommonInitialSubstring( |
| 149 | + lineInfo.map(info => info.left).filter(left => left !== null) |
| 150 | + )); |
| 151 | + this.text = lineInfo |
| 152 | + .map(info => (info.left?.substring(left.length) ?? '') + info.text) |
| 153 | + .join('\n'); |
| 154 | + } |
| 155 | + } |
| 156 | + |
| 157 | + clone(overrides?: Partial<SassCommentProps>): this { |
| 158 | + return utils.cloneNode(this, overrides, ['raws', 'silentText'], ['text']); |
| 159 | + } |
| 160 | + |
| 161 | + toJSON(): object; |
| 162 | + /** @hidden */ |
| 163 | + toJSON(_: string, inputs: Map<postcss.Input, number>): object; |
| 164 | + toJSON(_?: string, inputs?: Map<postcss.Input, number>): object { |
| 165 | + return utils.toJSON(this, ['text', 'text'], inputs); |
| 166 | + } |
| 167 | + |
| 168 | + /** @hidden */ |
| 169 | + toString( |
| 170 | + stringifier: postcss.Stringifier | postcss.Syntax = sassParser.scss |
| 171 | + .stringify |
| 172 | + ): string { |
| 173 | + return super.toString(stringifier); |
| 174 | + } |
| 175 | + |
| 176 | + /** @hidden */ |
| 177 | + get nonStatementChildren(): ReadonlyArray<Interpolation> { |
| 178 | + return []; |
| 179 | + } |
| 180 | +} |
| 181 | + |
| 182 | +interceptIsClean(SassComment); |
0 commit comments