Fix issue workspace names with apostrophes (#4848)
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
parent
bed624573d
commit
880fd964ac
|
|
@ -0,0 +1,18 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import { getLinkedWorkItemSuggestedName } from './mobile-workspace-name'
|
||||
|
||||
describe('mobile workspace names', () => {
|
||||
it('removes apostrophes inside words instead of splitting them', () => {
|
||||
expect(
|
||||
getLinkedWorkItemSuggestedName({
|
||||
title: "Can't enable browser notifications"
|
||||
})
|
||||
).toBe('cant-enable-browser-notifications')
|
||||
|
||||
expect(
|
||||
getLinkedWorkItemSuggestedName({
|
||||
title: 'Can’t enable browser notifications'
|
||||
})
|
||||
).toBe('cant-enable-browser-notifications')
|
||||
})
|
||||
})
|
||||
|
|
@ -5,8 +5,14 @@ export function resolveMobileWorkspaceCreateName(args: {
|
|||
return args.draft?.trim() || args.fallback
|
||||
}
|
||||
|
||||
// Why: mirrors the desktop issue-name slugger so contractions do not create
|
||||
// branch/path names like `can-t-enable` in mobile-created workspaces.
|
||||
function removeIntraWordApostrophes(input: string): string {
|
||||
return input.replace(/[‘’]/g, "'").replace(/([\p{L}\p{N}])'(?=[\p{L}\p{N}])/gu, '$1')
|
||||
}
|
||||
|
||||
function slugifyForWorkspaceName(input: string): string {
|
||||
return input
|
||||
return removeIntraWordApostrophes(input)
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[\\/]+/g, '-')
|
||||
|
|
|
|||
|
|
@ -13,6 +13,15 @@ describe('slugifyForWorkspaceName', () => {
|
|||
expect(slugifyForWorkspaceName('feature/add issue drawer')).toBe('feature-add-issue-drawer')
|
||||
expect(slugifyForWorkspaceName('a'.repeat(80))).toBe('a'.repeat(48))
|
||||
})
|
||||
|
||||
it('removes apostrophes inside words instead of splitting them', () => {
|
||||
expect(slugifyForWorkspaceName("Can't enable browser notifications")).toBe(
|
||||
'cant-enable-browser-notifications'
|
||||
)
|
||||
expect(slugifyForWorkspaceName('Can’t enable browser notifications')).toBe(
|
||||
'cant-enable-browser-notifications'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getLinkedWorkItemSuggestedName', () => {
|
||||
|
|
@ -90,6 +99,52 @@ describe('getWorkspaceIntentName', () => {
|
|||
})
|
||||
})
|
||||
|
||||
it('keeps contractions readable in linked issue display names', () => {
|
||||
expect(
|
||||
getWorkspaceIntentName({
|
||||
sourceText: 'https://github.com/acme/app/issues/4802',
|
||||
workItem: {
|
||||
type: 'issue',
|
||||
number: 4802,
|
||||
title: "Can't enable browser notifications from within a browser tab"
|
||||
}
|
||||
})
|
||||
).toEqual({
|
||||
displayName: "Issue 4802 Can't Enable Browser",
|
||||
seedName: 'issue-4802-cant-enable-browser'
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps single-letter contractions lowercase after the apostrophe', () => {
|
||||
expect(
|
||||
getWorkspaceIntentName({
|
||||
sourceText: 'https://github.com/acme/app/issues/17',
|
||||
workItem: {
|
||||
type: 'issue',
|
||||
number: 17,
|
||||
title: "i'm blocked on notifications"
|
||||
}
|
||||
})
|
||||
).toEqual({
|
||||
displayName: "Issue 17 I'm Blocked Notifications",
|
||||
seedName: 'issue-17-im-blocked-notifications'
|
||||
})
|
||||
|
||||
expect(
|
||||
getWorkspaceIntentName({
|
||||
sourceText: 'https://github.com/acme/app/issues/18',
|
||||
workItem: {
|
||||
type: 'issue',
|
||||
number: 18,
|
||||
title: "i'll update login"
|
||||
}
|
||||
})
|
||||
).toEqual({
|
||||
displayName: "Issue 18 I'll Update Login",
|
||||
seedName: 'issue-18-ill-update-login'
|
||||
})
|
||||
})
|
||||
|
||||
it('does not treat an auto-generated slug as explicit user intent', () => {
|
||||
expect(
|
||||
getWorkspaceIntentName({
|
||||
|
|
|
|||
|
|
@ -1,6 +1,22 @@
|
|||
function normalizeApostrophes(input: string): string {
|
||||
return input.replace(/[‘’]/g, "'")
|
||||
}
|
||||
|
||||
// Why: contractions and possessives should not become stray `t` / `s` tokens
|
||||
// in display names or extra hyphen segments in branch-safe workspace seeds.
|
||||
function removeIntraWordApostrophes(input: string): string {
|
||||
return normalizeApostrophes(input).replace(/([\p{L}\p{N}])'(?=[\p{L}\p{N}])/gu, '$1')
|
||||
}
|
||||
|
||||
function stripDanglingDisplayApostrophes(input: string): string {
|
||||
return normalizeApostrophes(input)
|
||||
.replace(/(^|[^\p{L}\p{N}])'(?=[\p{L}\p{N}])/gu, '$1')
|
||||
.replace(/([\p{L}\p{N}])'(?=$|[^\p{L}\p{N}])/gu, '$1')
|
||||
}
|
||||
|
||||
export function slugifyForWorkspaceName(input: string): string {
|
||||
return (
|
||||
input
|
||||
removeIntraWordApostrophes(input)
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[\\/]+/g, '-')
|
||||
|
|
@ -47,10 +63,7 @@ export type WorkspaceIntentName = {
|
|||
const ACTION_LABELS: [RegExp, string][] = [
|
||||
[/(?:^|[^a-z0-9_-])(?:fix(?:e[sd])?|resolve|repair)(?:$|[^a-z0-9_-])/i, 'Fix'],
|
||||
[/(?:^|[^a-z0-9_-])(?:debug|diagnose)(?:$|[^a-z0-9_-])/i, 'Debug'],
|
||||
[
|
||||
/(?:^|[^a-z0-9_-])(?:review|look\s+over|inspect|check|safe|safety)(?:$|[^a-z0-9_-])/i,
|
||||
'Review'
|
||||
],
|
||||
[/(?:^|[^a-z0-9_-])(?:review|look\s+over|inspect|check|safe|safety)(?:$|[^a-z0-9_-])/i, 'Review'],
|
||||
[/(?:^|[^a-z0-9_-])(?:implement|build|ship)(?:$|[^a-z0-9_-])/i, 'Implement'],
|
||||
[/(?:^|[^a-z0-9_-])(?:investigate|understand|triage)(?:$|[^a-z0-9_-])/i, 'Investigate'],
|
||||
[/(?:^|[^a-z0-9_-])(?:add|create)(?:$|[^a-z0-9_-])/i, 'Add'],
|
||||
|
|
@ -87,17 +100,26 @@ function detectIntentAction(sourceText: string): string | null {
|
|||
}
|
||||
|
||||
function titleCaseWord(word: string): string {
|
||||
const lower = word.toLowerCase()
|
||||
if (/^[A-Z]{2,}\d*$/.test(word) || /^[A-Z]+-\d+$/i.test(word)) {
|
||||
return word.toUpperCase()
|
||||
const normalized = normalizeApostrophes(word)
|
||||
if (/^[A-Z]{2,}\d*$/.test(normalized) || /^[A-Z]+-\d+$/i.test(normalized)) {
|
||||
return normalized.toUpperCase()
|
||||
}
|
||||
const acronymPossessive = normalized.match(/^([A-Z]{2,}\d*)'([sS])$/)
|
||||
if (acronymPossessive) {
|
||||
return `${acronymPossessive[1].toUpperCase()}'s`
|
||||
}
|
||||
const lower = normalized.toLowerCase()
|
||||
const apostropheParts = lower.split("'")
|
||||
if (apostropheParts.length === 2 && apostropheParts[0].length === 1 && apostropheParts[1]) {
|
||||
return `${apostropheParts[0].toUpperCase()}'${apostropheParts[1]}`
|
||||
}
|
||||
return lower.charAt(0).toUpperCase() + lower.slice(1)
|
||||
}
|
||||
|
||||
function compactWords(input: string, maxWords = 4): string {
|
||||
return input
|
||||
return stripDanglingDisplayApostrophes(input)
|
||||
.replace(/https?:\/\/\S+/gi, ' ')
|
||||
.replace(/[()[\]{}"']/g, ' ')
|
||||
.replace(/[()[\]{}"]/g, ' ')
|
||||
.replace(/[#/\\:_-]+/g, ' ')
|
||||
.split(/\s+/)
|
||||
.map((word) => word.trim())
|
||||
|
|
|
|||
Loading…
Reference in New Issue