-
Notifications
You must be signed in to change notification settings - Fork 91
Implement code and pre blocks support on web #456
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
jmusial
merged 173 commits into
main
from
@BartoszGrajdek/web-parser-refactor-code-blocks
Aug 4, 2025
Merged
Changes from 164 commits
Commits
Show all changes
173 commits
Select commit
Hold shift + click to select a range
f853061
Add refactor changes
Skalakid c4108c1
Fix parserUtils
Skalakid 38693c4
Add tree building when creating markdown HTML structure
Skalakid 6a1eaf1
Refactor function and type names
Skalakid 75d6afd
Fix TS errors
Skalakid db68b78
Update parser structure
Skalakid 31711ab
Move BrowserUtils into an object
Skalakid cab8ca7
Move utils to separate folder
Skalakid 2f829cc
Add block utils
Skalakid b9b2984
Move functions from above the web component to utils
Skalakid 3fbc456
Fix unit tests
Skalakid d74e0e4
Fix cursor positioning bugs
Skalakid 220395b
Remove scrollCursorIntoView function
Skalakid 4d95382
Rename variable names
Skalakid 907c73b
Replace textContent with ref value
Skalakid 1017ebc
Merge branch 'main' into @Skalakid/web-parser-refactor
Skalakid 1f1fefd
Merge branch 'main' into @Skalakid/web-parser-refactor
Skalakid ff41dd7
Fix crashes and cursor positioning in E/App
Skalakid d1d8a49
Fix copying and pasting text with markdown
Skalakid 153ead8
Fix pasting text starting with newlines
Skalakid ea6aad8
Fix errors when replacing text
Skalakid 87953bb
Fix HTML injestions and pasted text parsing
Skalakid 3d30621
Fix paste text trimming condiftion
Skalakid 471f53a
Fix cutting text
Skalakid 92a224c
Change handlePaste logic to fix newlines in pasted text
Skalakid c14add4
Fix cursor positioning when undoing/redoing previously pasted text
Skalakid e9d8ec3
Fix text coloring
Skalakid e24d259
Add review changes
Skalakid 092775d
Move updateTextColor function
Skalakid a0c1dfa
Fix cursor positioning when changing text and styles at the same time
Skalakid d04ad32
Fix newlines on FireFox
Skalakid 63de148
Fix cursor position value update when entering newline inside codeblock
Skalakid f06d10f
Fix removing characters when cursor is at the beginning of the line o…
Skalakid 34eabe2
Fix writing in empty line on Firefox
Skalakid a52e3f5
Fix cursor position value on Cmd+A on FireFox
Skalakid b5515cf
Fix getting value in e2e tests
Skalakid 5a043a3
Fix input e2e tests
Skalakid 5a1dae2
Fix style e2e tests
Skalakid 60f4f4b
Fix text manipulation e2e tests
Skalakid 55e78b4
Update checkCursorPosition function in e2e tests
Skalakid 8a0a7be
Update checkCursorPosition function in e2e tests
Skalakid 96a9b5c
Fix cursor position after redoing pasted text
Skalakid 8c85473
Merge branch '@Skalakid/web-parser-refactor' of github.com:Expensify/…
Skalakid 401febe
Merge branch 'main' into @Skalakid/web-parser-refactor
Skalakid 862e74a
Fix e2e tests on CI/CD
Skalakid 0580f45
Uncomment undo test
Skalakid 6682c9d
Fix TS errors
Skalakid 8165fb9
Change line merging funciton
Skalakid 9e2b943
Fix selection event sending on paste
Skalakid 0f5b86a
Merge branch 'main' into @Skalakid/web-parser-refactor
Skalakid 5f369ec
Fix scrolling cursor into view on Safari browser
Skalakid 7cdaf9a
Merge branch 'main' into @Skalakid/web-parser-refactor
Skalakid 6bada78
Enhance cursor positioning on input
Skalakid d9c097d
Merge branch 'main' into @Skalakid/web-parser-refactor
Skalakid 309bccf
Fix diacritics after CMD+A
Skalakid 90d2ea3
Fix autocorrect cursor positioning
Skalakid 111fd6b
Fix deleting codeBlock lines with CMD+backspace
Skalakid d14f4ba
Fix text color on undo/redo
Skalakid 82d58ca
Fix removing last letter from the line
Skalakid 2b9e56a
Fix cursor position when replacing text with the same text
Skalakid 5861a21
Change parseInnerHTMLToText function
Skalakid 68ae9be
Fix input behavior when interracting with display: block element
Skalakid 0bc882a
Fix getTreeNodeByIndex function
Skalakid cc8e307
Fix replacing whole content of the input
Skalakid 3da3989
Fix set cursor position when content changes
Skalakid ccb251e
Fix removing selection on paste
Skalakid e759170
Fix set cursor position on paste
Skalakid 4db4441
Fix dissapearing cursor bug
Skalakid 0480cb8
Fix pasting text into empty input
Skalakid 5faac50
Fix newline deletion
Skalakid 14d0eed
Improve parseInnerHTMLToText function and fix pasted text correct val…
Skalakid 0c204f5
Remove buildTree function
Skalakid de12aca
Fix tests
Skalakid 8648205
Replacing text by text cursor position
Skalakid eb1502a
Fix cursor positioning on custom text pasting (E/App)
Skalakid 3486303
Add function comments
Skalakid fd2be10
feat: code block background with proper handling
BartoszGrajdek df04ba0
feat: adjust styles for background width
BartoszGrajdek 3958bc3
feat: support for custom styles
BartoszGrajdek c57b91f
fix: refactor stylesheet handling
BartoszGrajdek 07ce864
chore: resolve merge conflicts
BartoszGrajdek 9485690
feat: improve styling
BartoszGrajdek b5a09d6
chore: resolve merge conflicts
BartoszGrajdek 5bea3f1
fix: paddings & autocorrect
BartoszGrajdek 5eb25b3
chore: resolve merge conflicts
BartoszGrajdek 457ea25
feat: add support for vertical & horizontal padding
BartoszGrajdek 561e0ec
fix: jumping background
BartoszGrajdek 6f1c32f
chore: merge main
BartoszGrajdek e54929e
fix: background issues
BartoszGrajdek 6379bb5
fix: cursor positioning
BartoszGrajdek a7fc650
fix: background sizing
BartoszGrajdek 3fa6fba
chore: resolve merge conflicts
BartoszGrajdek bb46ce0
fix: insufficient line-height in large code blocks
BartoszGrajdek cddf8a0
chore: resolve merge conflicts
BartoszGrajdek 7bc4ef0
fix: inline code parser problems
BartoszGrajdek 4aa355b
fix: performance & line-height
BartoszGrajdek 830b866
chore: resolve merge conflicts
BartoszGrajdek 1097a16
fix: resizing & styles not loading
BartoszGrajdek 32f1a1d
refactor: cleanup
BartoszGrajdek 351c32a
fix: code markdown fontSize in h1 tag
Skalakid b035335
chore: resolve merge conflicts
Skalakid c858c96
chore: merge main
Skalakid 1f2e93f
fix: tests
Skalakid e8b5e80
Mock RNweb funtions
Skalakid 89018dc
Remove unneccesary type export
Skalakid ee0609b
Merge branch 'main' into @BartoszGrajdek/web-parser-refactor-code-blocks
Skalakid 2d3fcf9
Add review changes
Skalakid ee67a71
Fix InputHistory after merge
Skalakid e2c2993
Move getPropertyValue
Skalakid 46818b0
Merge branch 'main' into @BartoszGrajdek/web-parser-refactor-code-blocks
Skalakid c0ff1ba
Merge branch 'main' into @BartoszGrajdek/web-parser-refactor-code-blocks
Skalakid 0a95808
Fix removing last new line in code block on Firefox
Skalakid a8b249b
Fix codeblock height when enetring different markdown in the same line
Skalakid 3b5161d
Make input parser fix only for Firefox
Skalakid 6775991
Fix flickering spellcheck
Skalakid 4c22b9d
Rename function
Skalakid 3b02ffe
Fix JS errors when value prop isn't passed
Skalakid edab956
Remove stylesheet when component is unmounted
Skalakid 884722a
Unify inline-code and pre styles2
Skalakid 6212c9c
Make border styles optional
Skalakid 0aff26d
Enhance style tests for codeblock
Skalakid d091bc9
Change codeblock HTML structure
Skalakid 7072394
Fix tests
Skalakid 3fbe395
Fix issue with multiple codeblocks in one input
Skalakid 5297311
Fix scrolling to the proper line inside codeblock when focusing an input
Skalakid c771ca7
Fix placing cursor at the end of first codeblock syntax
Skalakid 0924fb3
Fix selectable area inside codeblock on FireFox
Skalakid 26c82b3
Fix writing outside of codeblock borders
Skalakid a15826a
Add codeblock e2e tests
Skalakid ddbceed
Add codeblock content wrapping test
Skalakid f84890c
Add codeblock input resizing test
Skalakid d2d8c54
Fix codeblock dimensions test
Skalakid 6f8d873
Add test for scrolling codeblock into view
Skalakid e4efbdf
Fix scrolling to BR inside the codeblock
Skalakid 39740ad
Add delay after resizing the input
Skalakid 51d1d59
Fix display in global css file
Skalakid cab5ca6
Fix placing and focusing to the cursor at the end of the codeblock op…
Skalakid 5a5a2e9
Move isChildOfMarkdownElementTypes function to inputUtils
Skalakid 7512207
Fix newline generation after clearing the input
Skalakid c3af8d3
Fix breaking codeblock when removing first newline
Skalakid ac0bee5
Fix breaking codeblock when removing last newline
Skalakid 0d1b6d5
Move codeblock element styles to css file
Skalakid 5b6bab9
Fix breaking codeblock structure when deleting text before the closin…
Skalakid 44b59a7
Fix breaking codeblock when removing first newline
Skalakid c59982b
Update test names
Skalakid 35785eb
Add newline removal tests
Skalakid e343025
Fix codeblock tests structure
Skalakid 53050a4
Add content modification tests
Skalakid 8cdb8c3
Update code comments
Skalakid db4ab11
Fix removing newline at the end of codeblock on all platforms
Skalakid 5d2c50d
Fix problems with multiple codeblock merging together
Skalakid 34dbda2
Add codeblock styling checks
Skalakid 382ba4b
Add testCodeblock function to playwright tests
Skalakid 733115c
Merge branch 'main' into @BartoszGrajdek/web-parser-refactor-code-blocks
Skalakid 1ff2515
Add codeblock markdown count check
Skalakid 3aba838
Add newlines
Skalakid 67b97b5
Clean up code
Skalakid 7180aad
Refactor codeblock styles
Skalakid 6671add
Clean up code after refactor
Skalakid f2ed1c3
Remove newlines
Skalakid ddd456b
Update tests
Skalakid 6201427
Fix codeblock style test
Skalakid f3a0a12
Clean up web component changes
Skalakid 02bde24
Merge simillar styles
Skalakid c022083
Add review changes
Skalakid c91fdf2
Merge branch 'main' into @BartoszGrajdek/web-parser-refactor-code-blocks
Skalakid 93e87e4
Fix vertical and horizontal padding styles
Skalakid e45db44
Fix codeblocks in single line input
Skalakid d2acea0
Bump expensify-common
Skalakid 2c84ddd
Change minimum expensify-common version
Skalakid f134820
remove yarn from package.json
jmusial eed0fca
Merge remote-tracking branch 'upstream/main' into @BartoszGrajdek/web…
jmusial 8bd4cda
update package-lock
jmusial File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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,287 @@ | ||
import {test, expect} from '@playwright/test'; | ||
import type {Locator, Page} from '@playwright/test'; | ||
// eslint-disable-next-line import/no-relative-packages | ||
import * as TEST_CONST from '../../example/src/testConstants'; | ||
import {getElementValue, pressCmd, setCursorPosition, setupInput, testMarkdownContentStyle} from './utils'; | ||
|
||
const CODEBLOCK_DEFAULT_STYLE = | ||
'font-family: monospace; font-size: 20px; color: black; background-color: lightgray; border-color: gray; border-width: 1px; border-radius: 4px; border-style: solid; padding: 2px;'; | ||
|
||
async function testCodeblockStyle(page: Page, dimmensions: {height: number; width: number} | null, style?: string | null) { | ||
if (style === null) { | ||
await testMarkdownContentStyle({ | ||
testContent: 'Codeblock', | ||
style: 'margin: 0px; padding: 0px;', | ||
page, | ||
}); | ||
return; | ||
} | ||
await testMarkdownContentStyle({ | ||
testContent: 'Codeblock', | ||
style: style ?? CODEBLOCK_DEFAULT_STYLE, | ||
dimmensions: dimmensions ?? undefined, | ||
page, | ||
}); | ||
} | ||
|
||
async function getCodeblockElementCount(inputLocator: Locator) { | ||
return inputLocator.locator(`span[data-type="codeblock"]`).count(); | ||
} | ||
|
||
test.beforeEach(async ({page}) => { | ||
await page.goto(TEST_CONST.LOCAL_URL, {waitUntil: 'load'}); | ||
}); | ||
|
||
test.describe('modifying codeblock content', () => { | ||
test('keep newlines when writing after opening syntax', async ({page}) => { | ||
const inputLocator = await setupInput(page, 'clear'); | ||
await inputLocator.focus(); | ||
await inputLocator.pressSequentially('```\nCodeblock\nSample code line\n```'); | ||
|
||
await setCursorPosition(page, 0); | ||
await inputLocator.pressSequentially('test'); | ||
|
||
expect(await getElementValue(inputLocator)).toEqual('```test\nCodeblock\nSample code line\n```'); | ||
// Verify if the codeblock style wasn't applied | ||
await testCodeblockStyle(page, null, null); | ||
}); | ||
|
||
test('keep codeblock structure when writing in the empty last line', async ({page}) => { | ||
const inputLocator = await setupInput(page, 'clear'); | ||
await inputLocator.focus(); | ||
await inputLocator.pressSequentially('```\nCodeblock\nSample code line\n\n```'); | ||
|
||
await setCursorPosition(page, 6, 0); | ||
await inputLocator.pressSequentially('test'); | ||
|
||
expect(await getElementValue(inputLocator)).toEqual('```\nCodeblock\nSample code line\ntest\n```'); | ||
await testCodeblockStyle(page, { | ||
height: 84, | ||
width: 198, | ||
}); | ||
}); | ||
|
||
test('allow writing after closing syntax', async ({page}) => { | ||
const codeblockDimmensions = { | ||
height: 58, | ||
width: 198, | ||
}; | ||
const inputLocator = await setupInput(page, 'clear'); | ||
await inputLocator.focus(); | ||
await inputLocator.pressSequentially('```\nCodeblock\nSample code line\n```'); | ||
|
||
await setCursorPosition(page, 6); | ||
await testCodeblockStyle(page, codeblockDimmensions); | ||
await inputLocator.pressSequentially('test'); | ||
|
||
expect(await getElementValue(inputLocator)).toEqual('```\nCodeblock\nSample code line\n```test'); | ||
// Verify if when typing after codeblock closing syntax, its height is not changed | ||
await testCodeblockStyle(page, codeblockDimmensions); | ||
}); | ||
|
||
test('remove whole codeblock', async ({page}) => { | ||
const inputLocator = await setupInput(page, 'clear'); | ||
await inputLocator.focus(); | ||
await inputLocator.pressSequentially('```\nCodeblock\nSample code line\n```'); | ||
|
||
await pressCmd({inputLocator, command: 'a'}); | ||
await inputLocator.press('Backspace'); | ||
|
||
expect(await getElementValue(inputLocator)).toEqual(''); | ||
}); | ||
|
||
test('wrap content', async ({page}) => { | ||
const LINE_TO_ADD = ' very long line of code that should be wrapped'; | ||
const inputLocator = await setupInput(page, 'clear'); | ||
await inputLocator.focus(); | ||
await inputLocator.pressSequentially('```\nCodeblock\nSample code line\n```'); | ||
|
||
await setCursorPosition(page, 3); | ||
await inputLocator.pressSequentially(LINE_TO_ADD); | ||
|
||
expect(await getElementValue(inputLocator)).toEqual(`\`\`\`\nCodeblock${LINE_TO_ADD}\nSample code line\n\`\`\``); | ||
await testCodeblockStyle(page, { | ||
height: 110, | ||
width: 288, | ||
}); | ||
}); | ||
|
||
test('remove newline after opening syntax', async ({page}) => { | ||
const inputLocator = await setupInput(page, 'clear'); | ||
await inputLocator.focus(); | ||
await inputLocator.pressSequentially('```\nCodeblock\nSample code line\n```'); | ||
|
||
await setCursorPosition(page, 2, 0); | ||
await inputLocator.press('Backspace'); | ||
|
||
expect(await getElementValue(inputLocator)).toEqual('```Codeblock\nSample code line\n```'); | ||
// Verify if the codeblock style wasn't applied | ||
await testCodeblockStyle(page, null, null); | ||
}); | ||
|
||
test('remove newline after opening syntax with single line content', async ({page}) => { | ||
const inputLocator = await setupInput(page, 'clear'); | ||
await inputLocator.focus(); | ||
await inputLocator.pressSequentially('```\nCodeblock\n```'); | ||
|
||
await setCursorPosition(page, 2, 0); | ||
await inputLocator.press('Backspace'); | ||
|
||
expect(await getElementValue(inputLocator)).toEqual('```Codeblock\n```'); | ||
// Verify if the codeblock style wasn't applied | ||
await testCodeblockStyle(page, null, null); | ||
}); | ||
|
||
test('remove newline before closing syntax', async ({page}) => { | ||
const inputLocator = await setupInput(page, 'clear'); | ||
await inputLocator.focus(); | ||
await inputLocator.pressSequentially('```\nCodeblock\nSample code line\n```'); | ||
|
||
await setCursorPosition(page, 6, 0); | ||
await inputLocator.press('Backspace'); | ||
|
||
expect(await getElementValue(inputLocator)).toEqual('```\nCodeblock\nSample code line```'); | ||
// Verify if the codeblock style wasn't applied | ||
await testCodeblockStyle(page, null, null); | ||
}); | ||
|
||
test('remove newline before closing syntax with one empty line at the end', async ({page}) => { | ||
const inputLocator = await setupInput(page, 'clear'); | ||
await inputLocator.focus(); | ||
await inputLocator.pressSequentially('```\nCodeblock\nSample code line\n\n```'); | ||
|
||
await setCursorPosition(page, 6, 0); | ||
await inputLocator.press('Backspace'); | ||
|
||
expect(await getElementValue(inputLocator)).toEqual('```\nCodeblock\nSample code line\n```'); | ||
await testCodeblockStyle(page, { | ||
height: 58, | ||
width: 198, | ||
}); | ||
}); | ||
|
||
test('remove newline before closing syntax with two empy lines at the end', async ({page}) => { | ||
const inputLocator = await setupInput(page, 'clear'); | ||
await inputLocator.focus(); | ||
await inputLocator.pressSequentially('```\nCodeblock\nSample code line\n\n\n```'); | ||
|
||
await setCursorPosition(page, 6, 0); | ||
await inputLocator.press('Backspace'); | ||
|
||
expect(await getElementValue(inputLocator)).toEqual('```\nCodeblock\nSample code line\n\n```'); | ||
await testCodeblockStyle(page, { | ||
height: 84, | ||
width: 198, | ||
}); | ||
}); | ||
|
||
test('remove newline before opening syntax', async ({page}) => { | ||
const inputLocator = await setupInput(page, 'clear'); | ||
await inputLocator.focus(); | ||
await inputLocator.pressSequentially('\n\n```\nCodeblock\nSample code line\n```'); | ||
|
||
await setCursorPosition(page, 2, 0); | ||
await inputLocator.press('Backspace'); | ||
|
||
expect(await getElementValue(inputLocator)).toEqual('\n```\nCodeblock\nSample code line\n```'); | ||
await testCodeblockStyle(page, { | ||
height: 58, | ||
width: 198, | ||
}); | ||
}); | ||
|
||
test('remove newline between two codeblocks', async ({page}) => { | ||
const inputLocator = await setupInput(page, 'clear'); | ||
await inputLocator.focus(); | ||
await inputLocator.pressSequentially('```\nCodeblock\nSample code line\n```\n```\nCodeblock\nSecond sample code line\n```'); | ||
|
||
await setCursorPosition(page, 7, 0); | ||
await inputLocator.press('Backspace'); | ||
|
||
expect(await getElementValue(inputLocator)).toEqual('```\nCodeblock\nSample code line\n``````\nCodeblock\nSecond sample code line\n```'); | ||
expect(await getCodeblockElementCount(inputLocator)).toEqual(1); | ||
|
||
await inputLocator.press('Enter'); | ||
|
||
expect(await getElementValue(inputLocator)).toEqual('```\nCodeblock\nSample code line\n```\n```\nCodeblock\nSecond sample code line\n```'); | ||
expect(await getCodeblockElementCount(inputLocator)).toEqual(2); | ||
}); | ||
}); | ||
|
||
test('update codeblock dimensions when resizing the input', async ({page}) => { | ||
await page.setViewportSize({width: 1280, height: 720}); | ||
const inputLocator = await setupInput(page, 'clear'); | ||
await inputLocator.focus(); | ||
await inputLocator.pressSequentially('```\nCodeblock\nSample very long line of code that should be wrapped\n```'); | ||
|
||
await testCodeblockStyle(page, { | ||
height: 110, | ||
width: 288, | ||
}); | ||
|
||
await inputLocator.evaluate((inputElement: HTMLInputElement) => { | ||
const element = inputElement; | ||
element.style.width = '500px'; | ||
element.style.height = '200px'; | ||
}); | ||
await page.waitForTimeout(10); | ||
|
||
await testCodeblockStyle(page, { | ||
height: 84, | ||
width: 488, | ||
}); | ||
}); | ||
|
||
test.describe('scrolling into view', () => { | ||
test('scroll to an empty codeblock line', async ({page}) => { | ||
const inputLocator = await setupInput(page, 'clear'); | ||
await inputLocator.focus(); | ||
await inputLocator.evaluate((inputElement: HTMLInputElement) => { | ||
const element = inputElement; | ||
element.style.height = '100px'; | ||
}); | ||
await inputLocator.pressSequentially('```\nCodeblock start\n\n\n\n\n\n\n\n\nCodeblock end\n```'); | ||
|
||
await setCursorPosition(page, 4); | ||
await inputLocator.blur(); | ||
await inputLocator.evaluate((inputElement: HTMLInputElement) => { | ||
const element = inputElement; | ||
element.scrollTop = element.scrollHeight; | ||
return element.scrollHeight; | ||
}); | ||
|
||
await inputLocator.focus(); | ||
const scrollTop = await inputLocator.evaluate((inputElement: HTMLInputElement) => { | ||
const element = inputElement; | ||
return element.scrollTop; | ||
}); | ||
|
||
expect(scrollTop).toBeLessThanOrEqual(30); | ||
}); | ||
|
||
test('scroll to the cursor after opening syntax', async ({page}) => { | ||
const inputLocator = await setupInput(page, 'clear'); | ||
await inputLocator.focus(); | ||
await inputLocator.evaluate((inputElement: HTMLInputElement) => { | ||
const element = inputElement; | ||
element.style.height = '100px'; | ||
}); | ||
await inputLocator.pressSequentially('```\nCodeblock start\n\n\n\n\n\n\n\n\nCodeblock end\n```'); | ||
|
||
await setCursorPosition(page, 1); | ||
await inputLocator.blur(); | ||
await inputLocator.evaluate((inputElement: HTMLInputElement) => { | ||
const element = inputElement; | ||
element.scrollTop = element.scrollHeight; | ||
return element.scrollHeight; | ||
}); | ||
|
||
await inputLocator.focus(); | ||
const scrollTop = await inputLocator.evaluate((inputElement: HTMLInputElement) => { | ||
const element = inputElement; | ||
return element.scrollTop; | ||
}); | ||
|
||
expect(scrollTop).toBeLessThanOrEqual(25); | ||
}); | ||
}); |
This file contains hidden or 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 hidden or 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
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.