-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #253 from patricklx/fix-byte-issues
- Loading branch information
Showing
8 changed files
with
361 additions
and
87 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,104 +1,85 @@ | ||
import type { Parsed as RawGlimmerTemplate } from 'content-tag'; | ||
|
||
const EMPTY_SPACE = ' '; | ||
export interface Template { | ||
contents: string; | ||
type: string; | ||
range: { | ||
start: number; | ||
end: number; | ||
}; | ||
utf16Range: { | ||
start: number; | ||
end: number; | ||
}; | ||
} | ||
|
||
/** | ||
* Given a string (`original`), replaces the bytes in the given `range` with | ||
* equivalent bytes of empty space (' ') surrounded by the given prefix and | ||
* suffix. The total byte length will not change. | ||
* | ||
* Returns the resulting string. | ||
*/ | ||
function replaceByteRange( | ||
originalBuffer: Buffer, | ||
range: { start: number; end: number }, | ||
options: { prefix: string; suffix: string }, | ||
): string { | ||
const prefixBuffer = Buffer.from(options.prefix); | ||
const suffixBuffer = Buffer.from(options.suffix); | ||
const BufferMap: Map<string, Buffer> = new Map(); | ||
|
||
// Validate range | ||
if ( | ||
range.start < 0 || | ||
range.end > originalBuffer.length || | ||
range.start > range.end || | ||
prefixBuffer.length + suffixBuffer.length > range.end - range.start | ||
) { | ||
throw new Error( | ||
`Invalid byte range:\n\tstart=${range.start}\n\tend=${ | ||
range.end | ||
}\n\tprefix=${options.prefix}\n\tsuffix=${ | ||
options.suffix | ||
}\n\tstring=\n\t${originalBuffer.toString()}`, | ||
); | ||
function getBuffer(s: string): Buffer { | ||
let buf = BufferMap.get(s); | ||
if (!buf) { | ||
buf = Buffer.from(s); | ||
BufferMap.set(s, buf); | ||
} | ||
return buf; | ||
} | ||
|
||
// Adjust the space length to account for the prefix and suffix lengths | ||
const totalReplacementLength = range.end - range.start; | ||
const spaceLength = | ||
totalReplacementLength - prefixBuffer.length - suffixBuffer.length; | ||
|
||
// Create a buffer for the replacement | ||
const spaceBuffer = Buffer.alloc(spaceLength, EMPTY_SPACE); | ||
|
||
// Concatenate prefix, space, and suffix buffers | ||
const replacementBuffer = Buffer.concat([ | ||
prefixBuffer, | ||
spaceBuffer, | ||
suffixBuffer, | ||
]); | ||
|
||
// Create buffers for before and after the range using subarray | ||
const beforeRange = originalBuffer.subarray(0, range.start); | ||
const afterRange = originalBuffer.subarray(range.end); | ||
/** Slice string using byte range */ | ||
export function sliceByteRange(s: string, a: number, b?: number): string { | ||
const buf = getBuffer(s); | ||
return buf.subarray(a, b).toString(); | ||
} | ||
|
||
// Concatenate all parts and convert back to a string | ||
const result = Buffer.concat([beforeRange, replacementBuffer, afterRange]); | ||
/** Converts byte index to js char index (utf16) */ | ||
export function byteToCharIndex(s: string, byteOffset: number): number { | ||
const buf = getBuffer(s); | ||
return buf.subarray(0, byteOffset).toString().length; | ||
} | ||
|
||
if (result.length !== originalBuffer.length) { | ||
throw new Error( | ||
`Result length (${result.length}) does not match original length (${originalBuffer.length})`, | ||
); | ||
} | ||
/** Calculate byte length */ | ||
export function byteLength(s: string): number { | ||
return getBuffer(s).length; | ||
} | ||
|
||
return result.toString('utf8'); | ||
function replaceRange( | ||
s: string, | ||
start: number, | ||
end: number, | ||
substitute: string, | ||
): string { | ||
return sliceByteRange(s, 0, start) + substitute + sliceByteRange(s, end); | ||
} | ||
|
||
/** | ||
* Replace the template with a parsable placeholder that takes up the same | ||
* range. | ||
*/ | ||
export function preprocessTemplateRange( | ||
rawTemplate: RawGlimmerTemplate, | ||
template: Template, | ||
code: string, | ||
): string { | ||
const codeBuffer = Buffer.from(code); | ||
|
||
let prefix: string; | ||
let suffix: string; | ||
|
||
if (rawTemplate.type === 'class-member') { | ||
if (template.type === 'class-member') { | ||
// Replace with StaticBlock | ||
prefix = 'static{'; | ||
suffix = '}'; | ||
prefix = 'static{/*'; | ||
suffix = '*/}'; | ||
} else { | ||
// Replace with BlockStatement or ObjectExpression | ||
prefix = '{'; | ||
suffix = '}'; | ||
prefix = '{/*'; | ||
suffix = '*/}'; | ||
|
||
const nextToken = codeBuffer | ||
.subarray(rawTemplate.range.end) | ||
.toString() | ||
.match(/\S+/); | ||
const nextToken = code.slice(template.range.end).toString().match(/\S+/); | ||
if (nextToken && nextToken[0] === 'as') { | ||
// Replace with parenthesized ObjectExpression | ||
prefix = '(' + prefix; | ||
suffix = suffix + ')'; | ||
} | ||
} | ||
|
||
return replaceByteRange(codeBuffer, rawTemplate.range, { | ||
prefix, | ||
suffix, | ||
}); | ||
const content = template.contents.replaceAll('/', '\\/'); | ||
const tplLength = template.range.end - template.range.start; | ||
const spaces = | ||
tplLength - byteLength(content) - prefix.length - suffix.length; | ||
const total = prefix + content + ' '.repeat(spaces) + suffix; | ||
return replaceRange(code, template.range.start, template.range.end, total); | ||
} |
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 |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import Component from '@glimmer/component'; | ||
import { on } from '@ember/modifier'; | ||
|
||
import { getSnippetElement, toClipboard, withExtraStyles } from './copy-utils'; | ||
import Menu from './menu'; | ||
|
||
/** | ||
* This component is injected via the markdown rendering | ||
*/ | ||
export default class CopyMenu extends Component { | ||
copyAsText = (event: Event) => { | ||
let code = getSnippetElement(event); | ||
|
||
navigator.clipboard.writeText(code.innerText); | ||
}; | ||
|
||
copyAsImage = async (event: Event) => { | ||
let code = getSnippetElement(event); | ||
|
||
await withExtraStyles(code, () => toClipboard(code)); | ||
}; | ||
|
||
<template> | ||
<Menu data-test-copy-menu> | ||
<:trigger as |t|> | ||
<t.Default class="absolute top-3 right-4 z-10" data-test-copy-menu> | ||
📋 | ||
</t.Default> | ||
</:trigger> | ||
|
||
<:options as |Item|> | ||
<Item {{on "click" this.copyAsText}}> | ||
Copy as text | ||
</Item> | ||
<Item {{on "click" this.copyAsImage}}> | ||
Copy as image | ||
</Item> | ||
</:options> | ||
</Menu> | ||
</template> | ||
} |
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 |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import Component from '@glimmer/component'; | ||
|
||
/** | ||
* This component contains a multi-byte character | ||
*/ | ||
export default class MultiByteCharComponent extends Component { | ||
get rows() { | ||
console.log('abc다윤6') | ||
return [] | ||
} | ||
<template> | ||
{{#each this.rows as |row|}} | ||
{{row.id}} | ||
{{/each}} | ||
</template> | ||
} |
Oops, something went wrong.