mirror of
https://github.com/stablyai/orca.git
synced 2026-10-20 00:03:45 +00:00
fix(mobile): close exact-head review findings
This commit is contained in:
@@ -5,8 +5,7 @@ const EMBEDDED_DOCUMENT_FILE_RE = /(?:-html|webview-html)\.[cm]?[jt]sx?$/
|
||||
const USER_VISIBLE_ATTRIBUTE_RE =
|
||||
/\b(aria-description|aria-label|alt|data-placeholder|placeholder|title)\s*=\s*(["'])(.*?)\2/gi
|
||||
const USER_VISIBLE_PROMPT_RE = /\b(?:window\.)?(alert|confirm|prompt)\(\s*(["'])(.*?)\2/g
|
||||
const INSERTED_HTML_RE =
|
||||
/\bexecCommand\(\s*(["'])insertHTML\1\s*,\s*false\s*,\s*(["'])([\s\S]*?)\2\s*\)/g
|
||||
const INSERTED_HTML_CALL_RE = /\bexecCommand\(\s*(["'])insertHTML\1\s*,\s*false\s*,/g
|
||||
|
||||
function maskNonMarkupContent(documentText) {
|
||||
return documentText.replace(/<(script|style)\b[^>]*>[\s\S]*?<\/\1>/gi, (value) =>
|
||||
@@ -48,6 +47,99 @@ function collectMarkupCandidates(markup, baseOffset, candidates) {
|
||||
}
|
||||
}
|
||||
|
||||
function collectMarkupFragmentCandidates(markup, baseOffset, candidates) {
|
||||
collectMarkupCandidates(markup, baseOffset, candidates)
|
||||
|
||||
for (const match of [markup.match(/^([^<]+)</), markup.match(/>([^<]+)$/)]) {
|
||||
if (!match) {
|
||||
continue
|
||||
}
|
||||
const value = match[1]
|
||||
const start = baseOffset + capturedOffset(match, value)
|
||||
candidates.push({
|
||||
start,
|
||||
end: start + value.length,
|
||||
kind: 'embedded-html-text',
|
||||
text: value,
|
||||
dynamic: false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function skipQuotedString(source, start) {
|
||||
const quote = source[start]
|
||||
let cursor = start + 1
|
||||
while (cursor < source.length) {
|
||||
if (source[cursor] === '\\') {
|
||||
cursor += 2
|
||||
} else if (source[cursor] === quote) {
|
||||
return cursor + 1
|
||||
} else {
|
||||
cursor += 1
|
||||
}
|
||||
}
|
||||
return source.length
|
||||
}
|
||||
|
||||
function skipTemplateInterpolation(source, start) {
|
||||
let depth = 1
|
||||
let cursor = start + 2
|
||||
while (cursor < source.length && depth > 0) {
|
||||
if (source[cursor] === '"' || source[cursor] === "'") {
|
||||
cursor = skipQuotedString(source, cursor)
|
||||
} else {
|
||||
if (source[cursor] === '{') {
|
||||
depth += 1
|
||||
}
|
||||
if (source[cursor] === '}') {
|
||||
depth -= 1
|
||||
}
|
||||
cursor += 1
|
||||
}
|
||||
}
|
||||
return cursor
|
||||
}
|
||||
|
||||
function insertedHtmlArgumentEnd(source, start) {
|
||||
let depth = 1
|
||||
let cursor = start
|
||||
while (cursor < source.length) {
|
||||
if (source[cursor] === '"' || source[cursor] === "'") {
|
||||
cursor = skipQuotedString(source, cursor)
|
||||
continue
|
||||
}
|
||||
if (source[cursor] === '(') {
|
||||
depth += 1
|
||||
}
|
||||
if (source[cursor] === ')') {
|
||||
depth -= 1
|
||||
if (depth === 0) {
|
||||
return cursor
|
||||
}
|
||||
}
|
||||
cursor += 1
|
||||
}
|
||||
return source.length
|
||||
}
|
||||
|
||||
function collectInsertedHtmlCandidates(expression, baseOffset, candidates) {
|
||||
let cursor = 0
|
||||
while (cursor < expression.length) {
|
||||
if (expression.startsWith('${', cursor)) {
|
||||
cursor = skipTemplateInterpolation(expression, cursor)
|
||||
continue
|
||||
}
|
||||
if (expression[cursor] === '"' || expression[cursor] === "'") {
|
||||
const end = skipQuotedString(expression, cursor)
|
||||
const markup = expression.slice(cursor + 1, end - 1)
|
||||
collectMarkupFragmentCandidates(markup, baseOffset + cursor + 1, candidates)
|
||||
cursor = end
|
||||
continue
|
||||
}
|
||||
cursor += 1
|
||||
}
|
||||
}
|
||||
|
||||
export function collectMobileEmbeddedDocumentCandidates(filePath, sourceText) {
|
||||
if (!EMBEDDED_DOCUMENT_FILE_RE.test(filePath)) {
|
||||
return []
|
||||
@@ -85,10 +177,14 @@ export function collectMobileEmbeddedDocumentCandidates(filePath, sourceText) {
|
||||
})
|
||||
}
|
||||
|
||||
for (const match of documentText.matchAll(INSERTED_HTML_RE)) {
|
||||
const markup = match[3]
|
||||
const markupOffset = baseOffset + capturedOffset(match, markup)
|
||||
collectMarkupCandidates(markup, markupOffset, candidates)
|
||||
for (const match of documentText.matchAll(INSERTED_HTML_CALL_RE)) {
|
||||
const argumentStart = (match.index ?? 0) + match[0].length
|
||||
const argumentEnd = insertedHtmlArgumentEnd(documentText, argumentStart)
|
||||
collectInsertedHtmlCandidates(
|
||||
documentText.slice(argumentStart, argumentEnd),
|
||||
baseOffset + argumentStart,
|
||||
candidates
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ export const HTML = \`<!doctype html>
|
||||
<button>Copy</button><button>Select All</button>
|
||||
<script>
|
||||
window.prompt('Link URL');
|
||||
document.execCommand('insertHTML', false, '<p>Task</p>');
|
||||
document.execCommand('insertHTML', false, '<p>Task ' + localized + '</p><p>Follow up</p>');
|
||||
</script>
|
||||
\`
|
||||
`
|
||||
@@ -36,7 +36,8 @@ export const HTML = \`<!doctype html>
|
||||
'Copy',
|
||||
'Select All',
|
||||
'Link URL',
|
||||
'Task'
|
||||
'Task',
|
||||
'Follow up'
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -4,7 +4,7 @@ import { t } from '../i18n/mobile-i18n'
|
||||
import { MOBILE_RICH_MARKDOWN_KEYBOARD_INSET_SCRIPT } from './mobile-rich-markdown-editor-keyboard-inset-script'
|
||||
|
||||
export function escapeInjectedJavaScriptString(value: string): string {
|
||||
return JSON.stringify(value).replace(/<\/script/gi, '<\\/script')
|
||||
return JSON.stringify(value).replaceAll('<', '\\u003c')
|
||||
}
|
||||
|
||||
export function buildMobileRichMarkdownEditorHtml(): string {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
|
||||
import { buildMobileRichMarkdownEditorHtml } from '../components/mobile-rich-markdown-editor-html'
|
||||
import {
|
||||
buildMobileRichMarkdownEditorHtml,
|
||||
escapeInjectedJavaScriptString
|
||||
} from '../components/mobile-rich-markdown-editor-html'
|
||||
import { buildTerminalWebViewHtml } from '../terminal/terminal-webview-html'
|
||||
import { escapeEmbeddedHtmlCopy } from './embedded-webview-copy'
|
||||
import { mobileI18n } from './mobile-i18n'
|
||||
@@ -32,6 +35,10 @@ describe('embedded WebView copy', () => {
|
||||
expect(terminalHtml).toContain('id="sel-menu-all">Seleccionar todo</button>')
|
||||
})
|
||||
|
||||
it('escapes script-state delimiters in translated JavaScript strings', () => {
|
||||
expect(escapeInjectedJavaScriptString('<!--<script>')).toBe('"\\u003c!--\\u003cscript>"')
|
||||
})
|
||||
|
||||
it('HTML-escapes rich-editor placeholders before insertHTML parses them', () => {
|
||||
const codePlaceholder = mobileI18n.getResource(
|
||||
'en',
|
||||
|
||||
@@ -1739,7 +1739,7 @@
|
||||
"qE1-68U": "Creando reseña...",
|
||||
"JokOS5o": "Fuerza empujando con arrendamiento...",
|
||||
"0BMXLfE": "Impulsando commits...",
|
||||
"PnL-W2o": "Rama editorial...",
|
||||
"PnL-W2o": "Publicando rama...",
|
||||
"uN9IaYI": "Confirmando cambios...",
|
||||
"us6yszU": "Generando mensaje de commit...",
|
||||
"eCa7Me8": "Añadiendo cambios al staging...",
|
||||
@@ -1778,7 +1778,7 @@
|
||||
"7HjRY3c": "Commit",
|
||||
"koj5rtA": "Force Push",
|
||||
"63siVQ8": "Solicitud fallida: {{value0}}",
|
||||
"8I2nZuQ": "No se puede actualizar",
|
||||
"8I2nZuQ": "Omitido",
|
||||
"Ba1jOdE": "Neutral",
|
||||
"lyHptRM": "Tiempo agotado",
|
||||
"thrHxG4": "Cancelado",
|
||||
|
||||
@@ -145,7 +145,7 @@
|
||||
"6N6POoY": "PR の作成",
|
||||
"8fhgeQQ": "PR の準備中にブランチが変更されました。",
|
||||
"zAg6hUc": "commit メッセージを生成できませんでした。ソース管理に追加して、再試行してください。",
|
||||
"x1skfkY": "PR を作成する前に、変更を解決またはステージングします。",
|
||||
"x1skfkY": "PR を作成する前に、変更を解決するかステージングしてください。",
|
||||
"695pbL4": "PR を作成する前に競合を解決してください。",
|
||||
"QeyCTxE": "プッシュに失敗しました。プッシュエラーを解決してから、再試行してください。",
|
||||
"hjs2AB4": "Commits",
|
||||
@@ -161,7 +161,7 @@
|
||||
"s7LT0Wk": "agent の起動に失敗しました",
|
||||
"Ngsl81c": "デスクトップを待っています...",
|
||||
"h4RNhQ0": "commit メッセージの生成に失敗しました",
|
||||
"IhhRQcI": "PR を作成する前にブランチをチェックアウトします。",
|
||||
"IhhRQcI": "PR を作成する前にブランチをチェックアウトしてください。",
|
||||
"8lyq50g": "モバイルでソース管理を使用できるように Orca デスクトップを更新します。",
|
||||
"v0PMFRE": "比較のためのベース ブランチを解決できません。",
|
||||
"xLlZko0": "デスクトップを待っています...",
|
||||
@@ -1739,12 +1739,12 @@
|
||||
"qE1-68U": "レビューを作成しています...",
|
||||
"JokOS5o": "リースを使用して強制的にプッシュします...",
|
||||
"0BMXLfE": "commits をプッシュしています...",
|
||||
"PnL-W2o": "出版ブランチ...",
|
||||
"PnL-W2o": "ブランチを公開しています...",
|
||||
"uN9IaYI": "変更をコミットしています...",
|
||||
"us6yszU": "commit メッセージを生成中...",
|
||||
"eCa7Me8": "変更をステージしています...",
|
||||
"Kdbzhtg": "このブランチはまだ {{value0}} の準備ができていません。",
|
||||
"eeoaUzE": "{{value0}} を作成する前にベース ブランチをプッシュします。",
|
||||
"eeoaUzE": "{{value0}} を作成する前にベース ブランチをプッシュしてください。",
|
||||
"dhReg-4": "このフォークからの {{value0}} の作成はサポートされていません。",
|
||||
"UNrSqqU": "このブランチには {{value0}} がすでに存在します。",
|
||||
"CMzOREY": "この repo では {{value0}} の作成はサポートされていません。",
|
||||
@@ -1758,8 +1758,8 @@
|
||||
"nUw7_cE": "{{value0}} が作成されましたが、Orca はまだ更新できませんでした。",
|
||||
"89Qhlf4": "{{value0}} #{{value1}} はすでにオープンしています。",
|
||||
"G0fuKJo": "commit するものがありません。ブランチは最新です。",
|
||||
"qgmf_L8": "押す",
|
||||
"0OsPYDw": "引く",
|
||||
"qgmf_L8": "プッシュ",
|
||||
"0OsPYDw": "プル",
|
||||
"gSinyKw": "リモート ブランチを更新するには、リースを使用して強制的にプッシュします。",
|
||||
"5MUJYGA": "このブランチをオリジンに公開します。",
|
||||
"5OKMsK4": "commits を公開する前にブランチをチェックアウトしてください。",
|
||||
@@ -1778,7 +1778,7 @@
|
||||
"7HjRY3c": "Commit",
|
||||
"koj5rtA": "フォースプッシュ",
|
||||
"63siVQ8": "リクエストが失敗しました: {{value0}}",
|
||||
"8I2nZuQ": "更新できません",
|
||||
"8I2nZuQ": "スキップ済み",
|
||||
"Ba1jOdE": "中性",
|
||||
"lyHptRM": "タイムアウト",
|
||||
"thrHxG4": "キャンセル済み",
|
||||
@@ -1825,10 +1825,10 @@
|
||||
"N0HpMMg": "同期",
|
||||
"26SoVIw": "同期 (↓{{value0}} ↑{{value1}})",
|
||||
"Ia0aBfU": "何も引っ張るものがない",
|
||||
"0pGBpyQ": "引く",
|
||||
"0pGBpyQ": "プル",
|
||||
"oCECucM": "プル ({{value0}})",
|
||||
"zgbzIDs": "押し付けるものは何もない",
|
||||
"eBZzWkw": "押す",
|
||||
"eBZzWkw": "プッシュ",
|
||||
"9Xtk0Pg": "プッシュ ({{value0}})",
|
||||
"cBrXr-8": "Commit に失敗しました",
|
||||
"5jVDzOo": "デスクトップを待っています...",
|
||||
|
||||
@@ -1739,7 +1739,7 @@
|
||||
"qE1-68U": "리뷰 작성 중...",
|
||||
"JokOS5o": "임대로 강제 추진 중...",
|
||||
"0BMXLfE": "commits 푸시 중...",
|
||||
"PnL-W2o": "출판 브랜치...",
|
||||
"PnL-W2o": "브랜치 게시 중...",
|
||||
"uN9IaYI": "변경사항을 커밋하는 중...",
|
||||
"us6yszU": "commit 메시지 생성 중...",
|
||||
"eCa7Me8": "변경 사항을 스테이징하는 중...",
|
||||
@@ -1778,7 +1778,7 @@
|
||||
"7HjRY3c": "Commit",
|
||||
"koj5rtA": "강제 푸시",
|
||||
"63siVQ8": "요청 실패: {{value0}}",
|
||||
"8I2nZuQ": "업데이트할 수 없음",
|
||||
"8I2nZuQ": "건너뜀",
|
||||
"Ba1jOdE": "중립",
|
||||
"lyHptRM": "시간 초과",
|
||||
"thrHxG4": "취소됨",
|
||||
|
||||
@@ -1736,7 +1736,7 @@
|
||||
"qE1-68U": "正在创建评论...",
|
||||
"JokOS5o": "用租约强制推动...",
|
||||
"0BMXLfE": "推动 commits...",
|
||||
"PnL-W2o": "出版分社...",
|
||||
"PnL-W2o": "正在发布分支...",
|
||||
"uN9IaYI": "正在提交更改...",
|
||||
"us6yszU": "正在生成 commit 消息...",
|
||||
"eCa7Me8": "正在暂存更改...",
|
||||
@@ -1775,7 +1775,7 @@
|
||||
"7HjRY3c": "Commit",
|
||||
"koj5rtA": "强力推",
|
||||
"63siVQ8": "请求失败:{{value0}}",
|
||||
"8I2nZuQ": "无法更新",
|
||||
"8I2nZuQ": "已跳过",
|
||||
"Ba1jOdE": "Neutral",
|
||||
"lyHptRM": "已超时",
|
||||
"thrHxG4": "已取消",
|
||||
|
||||
@@ -94,16 +94,22 @@ describe('mobile i18n', () => {
|
||||
await mobileI18n.changeLanguage('ja')
|
||||
|
||||
expect([
|
||||
t('m.x1skfkY'),
|
||||
t('m.IhhRQcI'),
|
||||
t('m.-CpqM4g', { value0: 'PR' }),
|
||||
t('m.Kdbzhtg', { value0: 'PR' }),
|
||||
t('m.eeoaUzE', { value0: 'PR' }),
|
||||
t('m.i65iwXo', { value0: 'PR' }),
|
||||
t('m.h5c6qGM', { value0: 'PR' }),
|
||||
t('m.aDj0ISs', { value0: 'PR' }),
|
||||
t('m.eL3YqZQ', { value0: 'PR' }),
|
||||
t('m.fS2uz3k', { value0: 'PR' })
|
||||
]).toEqual([
|
||||
'PR を作成する前に、変更を解決するかステージングしてください。',
|
||||
'PR を作成する前にブランチをチェックアウトしてください。',
|
||||
'このブランチはまだ PR の準備ができていません。',
|
||||
'このブランチはまだ PR の準備ができていません。',
|
||||
'PR を作成する前にベース ブランチをプッシュしてください。',
|
||||
'認証してから PR を作成してください。',
|
||||
'このブランチを同期してから PR を作成してください。',
|
||||
'コミットを公開してから PR を作成してください。',
|
||||
@@ -112,6 +118,29 @@ describe('mobile i18n', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it.each([
|
||||
['es', ['Omitido', 'Publicando rama...']],
|
||||
['ja', ['スキップ済み', 'ブランチを公開しています...']],
|
||||
['ko', ['건너뜀', '브랜치 게시 중...']],
|
||||
['zh', ['已跳过', '正在发布分支...']]
|
||||
] satisfies [MobileUiLocale, string[]][])(
|
||||
'%s uses Git operation terminology',
|
||||
async (locale, expected) => {
|
||||
await mobileI18n.changeLanguage(locale)
|
||||
expect([t('m.8I2nZuQ'), t('m.PnL-W2o')]).toEqual(expected)
|
||||
}
|
||||
)
|
||||
|
||||
it('uses Git push and pull terminology in Japanese', async () => {
|
||||
await mobileI18n.changeLanguage('ja')
|
||||
expect([t('m.qgmf_L8'), t('m.0OsPYDw'), t('m.eBZzWkw'), t('m.0pGBpyQ')]).toEqual([
|
||||
'プッシュ',
|
||||
'プル',
|
||||
'プッシュ',
|
||||
'プル'
|
||||
])
|
||||
})
|
||||
|
||||
it.each([
|
||||
['es', ['Sin comprobaciones', 'En staging', 'Nota de revisión']],
|
||||
['ja', ['チェックなし', 'ステージ済み', 'レビューメモ']],
|
||||
|
||||
Reference in New Issue
Block a user