generated from norskeld/serpent
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
197 additions
and
13 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,17 +1,111 @@ | ||
import { describe, it, expect } from 'vitest' | ||
|
||
import { identity } from './index' | ||
import un from './index' | ||
|
||
describe('index.ts', () => { | ||
it('given a primitive, should return the same primitive', async () => { | ||
expect(await identity('string')).toBe('string') | ||
expect(await identity(1986)).toBe(1986) | ||
expect(await identity(true)).toBe(true) | ||
expect(await identity(undefined)).toBe(undefined) | ||
function concat(...segments: Array<string>): string { | ||
return segments.join('\n') + '\n' | ||
} | ||
|
||
describe('un (tagged)', () => { | ||
it('should unindent if given a multiline string', () => { | ||
const actual = un` | ||
begin | ||
- indented line | ||
- indented line | ||
end | ||
` | ||
|
||
// prettier-ignore | ||
const expected = concat( | ||
'begin', | ||
' - indented line', | ||
' - indented line', | ||
'end' | ||
) | ||
|
||
expect(actual).toBe(expected) | ||
}) | ||
|
||
it('should unindent if given a multiline string that starts right after quote/backtick', () => { | ||
const actual = un`begin | ||
- indented line | ||
- indented line | ||
end | ||
` | ||
|
||
// prettier-ignore | ||
const expected = concat( | ||
'begin', | ||
' - indented line', | ||
' - indented line', | ||
'end' | ||
) | ||
|
||
expect(actual).toBe(expected) | ||
}) | ||
|
||
it('should return as-is if given a single-line string', () => { | ||
const actual = un`first` | ||
const expected = `first` | ||
|
||
expect(actual).toBe(expected) | ||
}) | ||
|
||
it('should throw if provided with not a string or template strings array', () => { | ||
// Creating bare object that doesn't have any means in the prototype chain to let it be | ||
// converted into string (toString/toPrimitive). | ||
const bare = Object.create(null) | ||
|
||
expect(() => un`${bare as unknown as string}`).toThrow() | ||
}) | ||
}) | ||
|
||
describe('un (untagged)', () => { | ||
it('should unindent given multiline string', () => { | ||
const actual = un(` | ||
begin | ||
- indented line | ||
- indented line | ||
end | ||
`) | ||
|
||
// prettier-ignore | ||
const expected = concat( | ||
'begin', | ||
' - indented line', | ||
' - indented line', | ||
'end' | ||
) | ||
|
||
expect(actual).toBe(expected) | ||
}) | ||
|
||
it('should unindent if given a multiline string that starts right after quote/backtick', () => { | ||
const actual = un(`begin | ||
- indented line | ||
- indented line | ||
end | ||
`) | ||
|
||
// prettier-ignore | ||
const expected = concat( | ||
'begin', | ||
' - indented line', | ||
' - indented line', | ||
'end' | ||
) | ||
|
||
expect(actual).toBe(expected) | ||
}) | ||
|
||
it('should return as-is if given a single-line string', () => { | ||
const actual = un('first') | ||
const expected = 'first' | ||
|
||
expect(actual).toBe(expected) | ||
}) | ||
|
||
it('given an object, should return the same object', async () => { | ||
const value = { foo: 'bar' } | ||
expect(await identity(value)).toBe(value) | ||
it('should throw if provided with not a string or template strings array', () => { | ||
expect(() => un({} as unknown as string)).toThrow() | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,89 @@ | ||
export async function identity<T>(value: T): Promise<T> { | ||
return value | ||
const NG_ONE = -1 | ||
|
||
const Chars = { | ||
/** Space. */ | ||
SP: '\u0020', | ||
/** Horizontal tab. */ | ||
HT: '\u0009', | ||
/** Line feed (Unix/macOS). */ | ||
LF: '\u000A', | ||
/** Carriage return + Line feed (Windows). */ | ||
CRLF: '\u000D\u000A' | ||
} as const | ||
|
||
function nonNegative(count: number): boolean { | ||
return count > NG_ONE | ||
} | ||
|
||
function toSpacesCount(line: string) { | ||
const chars = [...line].entries() | ||
|
||
for (const [idx, char] of chars) { | ||
if (char !== Chars.SP && char !== Chars.HT) { | ||
return idx | ||
} | ||
} | ||
|
||
return NG_ONE | ||
} | ||
|
||
function un$untagged(s: string): string { | ||
// Document may start either on the same line as opening quote/backtick or on the next line. | ||
const shouldIgnoreFirstLine = s.startsWith(Chars.LF) | ||
|
||
// TODO: Handle the `\r\n` case as well. | ||
const lines = s.split(Chars.LF) | ||
|
||
// Get everything but the 1st line. | ||
const [, ...rest] = lines | ||
|
||
// Get number of spaces for each line and leave only non-negative values. | ||
const counts = rest.length ? rest.map(toSpacesCount).filter(nonNegative) : [0] | ||
|
||
// Largest number of spaces that can be removed from every non-whitespace-only line after the 1st. | ||
const spaces = Math.min(...counts) | ||
|
||
// Resulting string. | ||
let result = String() | ||
|
||
for (const [idx, line] of lines.entries()) { | ||
if (idx > 1 || (idx === 1 && !shouldIgnoreFirstLine)) { | ||
result += Chars.LF | ||
} | ||
|
||
// Do not unindent anything on same line as opening quote/backtick. | ||
if (idx === 0) { | ||
result += line | ||
} | ||
// Whitespace-only lines may have fewer than the number of spaces being removed. | ||
else if (line.length > spaces) { | ||
result += line.slice(spaces) | ||
} | ||
} | ||
|
||
return result | ||
} | ||
|
||
function un$tagged(strings: TemplateStringsArray, ...values: Array<unknown>): string { | ||
return un$untagged(String.raw({ raw: strings }, ...values)) | ||
} | ||
|
||
/** | ||
* Unindents multiline string. | ||
* | ||
* This function takes a multiline string and unindents it so the leftmost non-space character is in | ||
* the first column. | ||
*/ | ||
function un(s: string): string | ||
function un(s: TemplateStringsArray, ...values: Array<unknown>): string | ||
function un(s: string | TemplateStringsArray, ...values: Array<unknown>): string { | ||
if (typeof s === 'string') { | ||
return un$untagged(s) | ||
} else if (Array.isArray(s)) { | ||
return un$tagged(s, ...values) | ||
} | ||
|
||
throw new Error(`Only 'string' and 'template strings array' allowed, but got '${typeof s}'.`) | ||
} | ||
|
||
export default un |