URL 조각 텍스트 지시문

커뮤니티 그룹 초안 보고서,

현재 버전:
https://wicg.github.io/scroll-to-text-fragment/
이슈 추적:
GitHub
명세 내 인라인
편집자:
(Google)
(Google)
테스트 모음:
https://wpt.fyi/results/scroll-to-text-fragment/

초록

텍스트 지시문은 URL 조각에서 텍스트 스니펫을 명시하는 기능을 추가합니다. 이러한 조각이 포함된 URL로 이동할 때, 사용자 에이전트는 해당 부분을 빠르게 강조 표시하거나 사용자의 주의를 끌 수 있습니다.

이 문서의 상태

이 명세는 Web Platform Incubator Community Group에서 발행하였습니다. W3C 표준이 아니며 W3C 표준 트랙에도 속하지 않습니다. W3C 커뮤니티 기여자 사용권 계약(CLA)에 따라 제한적 거부(opt-out) 및 기타 조건이 적용됨을 참고하십시오. W3C 커뮤니티 및 비즈니스 그룹에 대해 자세히 알아보세요.

1. 인프라스트럭처

이 명세는 Infra Standard에 의존합니다. [INFRA]

2. 소개

이 섹션은 비규범적입니다

2.1. 사용 사례

2.1.1. 웹 텍스트 참조

텍스트 조각의 핵심 사용 사례는 URL이 웹 전체에서 정확한 텍스트 참조로 사용될 수 있도록 하는 것입니다. 예를 들어, Wikipedia 참조는 인용하는 페이지의 정확한 텍스트로 연결될 수 있습니다. 유사하게, 검색 엔진은 사용자가 원하는 답이 페이지 내 어디에 있는지 바로 안내하는 URL을 제공할 수 있습니다. 이는 페이지 맨 위로 이동시키는 링크 대신입니다.

2.1.2. 사용자 공유

텍스트 지시문이 있으면, 브라우저는 사용자가 텍스트를 선택 후 컨텍스트 메뉴를 열 때 '여기로 URL 복사' 옵션을 구현할 수 있습니다. 그 후 브라우저는 해당 텍스트 선택이 명확하게 지정된 URL을 생성할 수 있고, URL을 받은 사람은 지정된 텍스트가 편리하게 표시됩니다. 텍스트 조각이 없다면 사용자가 페이지에서 텍스트 일부를 공유하고 싶을 때, 아마도 단순히 그 일부를 복사해 붙여넣기 할 것이며, 이 경우 수신자는 페이지의 맥락(context)를 잃게 됩니다.

이 명세는 텍스트 지시문 링크의 유효 수명을 극대화하려고 시도합니다. 예를 들어, 실제 텍스트 내용을 URL 페이로드로 사용하고, 필요시 fallback 엘리먼트 ID 조각을 허용합니다. 하지만 웹의 페이지들은 종종 업데이트되고 그 내용을 변경합니다. 따라서 이런 링크들은 가리키는 텍스트가 더 이상 대상 페이지에 존재하지 않아 "썩는(rot)" 현상이 있을 수 있습니다.

이런 문제에도 불구하고 텍스트 지시문 링크는 유용할 수 있습니다. 사용자 공유 같은 use case에서는, 링크가 일시적(transient)이며, 전송 후 바로 사용되는 용도에 적합합니다. 참고 문헌이나 웹 페이지 링크처럼 오랜 기간 사용되는 경우, 텍스트 지시문은 점진적으로 일반 링크로 동작하므로 여전히 유용합니다. 추가로, 오래된(stale) 텍스트 지시문이 표시되면 사용자가 링크 생성자의 원래 의도를 이해하고, 링크가 생성된 이후 페이지 내용이 변경되었음을 알 수 있는 정보가 됩니다.

견고한 텍스트 지시문 링크 생성의 모범 사례는 § 4 텍스트 조각 지시문 생성을 참고하세요.

3. 설명

3.1. 표시

이 섹션은 비규범적입니다

이 명세는 텍스트 일치 항목을 "표시"할 때 사용자 에이전트가 무엇을 해야 하는지 구체적으로 정의하지 않습니다. 사용자 에이전트마다 다양한 경험과 선택지가 있습니다. 가능한 동작의 예시:

동작 선택에 따라 사용자 보안 및 프라이버시에 영향을 줄 수 있습니다. 자세한 사항은 § 3.5 보안 및 프라이버시를 참조하세요.

3.2. 구문

이 섹션은 비규범적입니다

텍스트 지시문조각 지시문(§ 3.3 The Fragment Directive 참고)에 다음과 같은 형식으로 지정됩니다:

#:~:text=[prefix-,]start[,end][,-suffix]
          context  |--match--|  context

(대괄호는 선택적 매개변수를 의미합니다)

텍스트 매개변수는 매칭 이전에 퍼센트 디코딩됩니다. Dash(-), ampersand(&), comma(,)는 지시문 구문으로 오해받지 않도록 퍼센트 인코딩됩니다.

필수 매개변수는 start 뿐입니다. start만 지정된 경우, 정확히 일치하는 첫 텍스트 문자열이 대상이 됩니다.

#:~:text=an%20example%20text%20fragment은 정확히 "an example text fragment"가 대상 텍스트임을 나타냅니다.

end 매개변수도 함께 지정되면, 텍스트 지시문은 페이지 내의 텍스트 범위를 참조하게 됩니다. 대상 텍스트 범위는 첫 start 발생 위치에서 바로 뒤따르는 end의 첫 발생 위치까지입니다. 이는 전체 텍스트 범위를 start에 명시하는 것과 같으나, 긴 지시문 사용을 피할 수 있습니다.

#:~:text=an%20example,text%20fragment은 페이지 내에서 "an example" 시작 후 "text fragment"까지가 대상이 됨을 의미합니다.

3.2.1. 문맥 용어

이 섹션은 비규범적입니다

두 개의 다른 선택적 매개변수는 문맥 용어(context terms)입니다. prefix 뒤와 suffix 앞의 dash(-)로 구분되어, 어떤 조합의 선택 매개변수도 지정할 수 있도록 startend와 구별됩니다.

문맥 용어는 대상 텍스트 조각을 명확하게 식별하는 데 사용됩니다. 문맥 용어는 조각 바로 앞(prefix)이나 뒤(suffix)의 텍스트를 지정하며, 공백은 허용됩니다.

문맥 용어가 대상 텍스트 조각을 둘러싸야(앞뒤에 있어야) 일치가 성공하지만, 문맥과 텍스트 조각 사이의 공백은 얼마든 허용됩니다. 예를 들어, 대상 조각이 단락의 시작 부분이고 이전 엘리먼트의 텍스트가 프리픽스 역할을 해야 하는 경우에도 문맥 용어를 사용할 수 있습니다.

문맥 용어는 매칭 대상인 텍스트 조각에는 포함되지 않으며, 시각적으로 표시되지도 않습니다.

#:~:text=this%20is-,an%20example,-text%20fragment는 "this is an example text fragment"에서 "an example"에 일치하지만, "here is an example text"에는 일치하지 않습니다.

3.2.2. 양방향(BiDi) 고려사항

이 섹션은 비규범적입니다
유니코드 양방향 알고리즘(Unicode Bidirectional Algorithm) 기초를 참고하세요.

URL 문자열은 ASCII로 인코딩되므로, 내장된 양방향 텍스트 지원이 없습니다. 하지만, 페이지의 대상 텍스트는 LTR(왼쪽→오른쪽), RTL(오른쪽→왼쪽), 또는 둘 다(BiDi)일 수 있습니다. 본 섹션에선 명세의 규범적 부분에서 암시된 동작을 직관적으로 설명합니다.

텍스트 조각 내 각 용어의 문자는 논리적 순서로 배치됩니다. 즉, 원어 사용자가 읽는 순서(메모리 저장 순서)입니다.

마찬가지로 prefixstart 용어는 논리적 순서 상 앞에 오는 문자열을, suffixend는 논리적 순서 상 뒤에 오는 문자열을 지정합니다.

참고: 사용자 에이전트는, 예를 들어 표시 문자열을 유니코드로 변환함으로써, 원어 사용자에게 더 친숙하게 URL을 렌더링할 수 있습니다. 하지만 URL의 문자열 표현은 오직 ASCII 문자로 유지됩니다.

만약 مِصر‎(이집트, 아랍어)을 선택하고, 그 앞에 البحرين‎(바레인, 아랍어)이 있을 경우라고 가정해봅시다. 각 용어를 퍼센트 인코딩하면:

مِصر‎는 "%D9%85%D8%B5%D8%B1"이 됩니다. (참고: UTF-8 문자 [0xD9,0x85]가 아랍어 단어의 첫(오른쪽) 문자)

البحرين‎는 "%D8%A7%D9%84%D8%A8%D8%AD%D8%B1%D9%8A%D9%86"이 됩니다.

텍스트 조각은 이렇게 됩니다:

:~:text=%D8%A7%D9%84%D8%A8%D8%AD%D8%B1%D9%8A%D9%86-,%D9%85%D8%B5%D8%B1

브라우저 주소창에서는 브라우저가 자연스러운 RTL 방향으로 시각적 렌더링을 하므로 사용자에게는 다음과 같이 보일 수 있습니다:

:~:text=البحرين-,مِصر

3.3. 조각 지시문

기존 URL 조각 사용과의 호환성 문제를 피하기 위해, 본 명세는 조각 지시문 개념을 도입합니다. 이것은 URL 조각조각 지시문 구분자 뒤에 오는 부분이며, 구분자가 조각에 나타나지 않은 경우 null일 수 있습니다.

조각 지시문 구분자는 문자열 ":~:"입니다. 즉, 연속된 3개의 코드포인트 U+003A(:), U+007E(~), U+003A(:)입니다.

조각 지시문은 URL 조각의 일부입니다. 즉, 항상 URL에서 U+0023(#) 코드포인트 뒤에 옵니다.
조각 지시문을 https://example.com 같은 URL에 추가하려면, 먼저 조각을 추가합니다: https://example.com#:~:text=foo.

조각 지시문은 개별 지시문으로 파싱되어 처리되며, 이는 사용자 에이전트가 어떤 동작을 수행해야 하는지 안내하는 명령입니다. 여러 개의 지시문이 조각 지시문에 나타날 수 있습니다.

이 명세에서 소개된 지시문은 텍스트 지시문 뿐이지만, 미래에는 추가될 수 있습니다.
https://example.com#:~:text=foo&text=bar&unknownDirective

텍스트 지시문 2개와 알 수 없는 지시문 1개를 포함합니다.

페이지 동작에 영향을 주지 않도록, 스크립트에서 접근 가능한 API에선 제거됩니다. 이는 미래 지시문이 웹 호환성 위험 없이 추가될 수 있도록 해줍니다.

3.3.1. 조각 지시문 추출

이 섹션에서는 조각 지시문이 스크립트에서 어떻게 숨겨지는지와 HTML § 7.4 탐색 및 세션 기록에 어떻게 적용되는지를 설명합니다.

이 섹션에서 요약된 변경 사항:

HTML § 7.4.1 세션 기록에서 지시문 상태를 정의:

HTML § 7.4.1 세션 기록에 대한 몽키패치:

지시문 상태 는 세션 기록 항목이 생성될 당시의 조각 지시문 값을 저장하며, 해당 항목에 접근할 때마다(탐색 시 등) 지시문(예: 텍스트 하이라이트 등)을 적용하는 데 사용됩니다. 다음을 가집니다:

여러 세션 기록 항목이 하나의 지시문 상태를 공유할 수 있습니다.

조각 지시문은 URL이 세션 기록 항목에 설정되기 전에 URL에서 제거됩니다. 대신 지시문 상태에 저장됩니다. 이것은 스크립트 API에서 지시문이 보이지 않도록 하여, 페이지 동작에 영향을 주지 않고 지시문만 지정할 수 있게 합니다.

조각 지시문은 순수 문자열 대신 지시문 상태 오브젝트에 저장됩니다. 왜냐하면 같은 지시문 상태가 여러 인접 세션 기록 항목에 공유될 수 있기 때문입니다. 탐색 시, 두 항목 사이에서 지시문 상태가 변경된 경우에만(예: 텍스트 검색 및 하이라이트 등) 지시문이 적용됩니다.

세션 기록 항목의 정의에 추가:

HTML § 7.4.1.1 세션 기록 항목에 대한 몽키패치:

세션 기록 항목은 다음 항목들로 이루어진 구조체입니다:
  • ...

  • 영속화된 사용자 상태(persisted user state), 구현에 따라 정의되며 초깃값은 null

  • 지시문 상태, 지시문 상태, 초깃값은 새로운 지시문 상태

URL에서 조각 지시문 문자열을 제거하고 반환하는 헬퍼 알고리즘 추가:

[HTML]에 대한 몽키패치:

이 알고리즘은 URL의 프래그먼트가 조각 지시문 구분자에서 끝나게 합니다. 반환되는 조각 지시문은 구분자 뒷부분 전체를 포함하며 구분자 자체는 포함하지 않습니다.
TODO: 만약 URL의 프래그먼트가 ':~:'로 끝나면(즉, 빈 지시문), null이 반환되며 명시적 지시문을 지정하지 않은 것으로 간주합니다(기존 값 덮어쓰기 방지). 그러나 이 경우 빈 문자열을 반환하는 것이 더 좋을까요? 이렇게 하면 '#:~:'로 이동/푸시했을 때 명확히 지시문/하이라이트를 초기화할 수 있습니다.

조각 지시문 제거 알고리즘은 URL url에서 아래 단계로 진행합니다:

  1. raw fragmenturlfragment로 둡니다.

  2. fragment directive를 null로 둡니다.

  3. 만약 raw fragment가 null이 아니고 조각 지시문 구분자가 substring으로 포함되어 있다면:

    1. position문자 위치 변수로 설정하며, raw fragment조각 지시문 구분자가 처음 나타나는 첫 코드 포인트 또는 raw fragment 끝을 가리키게 합니다.

    2. new fragment문자 위치별 부분 문자열로 설정합니다. raw fragment의 시작부터 position까지입니다.

    3. position구분자 코드 포인트 길이만큼을 더합니다 (조각 지시문 구분자 길이).

    4. 만약 positionraw fragment 끝을 넘지 않는다면:

      1. fragment directive끝까지 부분 문자열(raw fragmentposition부터 끝까지)로 설정

    5. urlfragmentnew fragment로 설정합니다.

  4. fragment directive를 반환합니다.

https://example.org/#test:~:text=foo는 fragment는 "test"이고 조각 지시문은 "text=foo"로 파싱됩니다.

다음 네 가지 몽키패치는, URL에 조각 지시문을 포함할 가능성이 있을 때 세션 히스토리 항목을 생성할 때, 조각 지시문을 제거하고 지시문 상태에 저장하는 부분을 수정합니다.

navigate 정의에서:

HTML § 7.4.2.2 탐색 시작에 대한 몽키패치:

내비게이션 가능한 대상으로 URL url을 탐색하려면...:
  1. ...

  2. navigationId를 ongoing navigation에 설정
  3. url 스킴이 "javascript"일 경우...

  4. 다음 단계들을 병렬로 실행:

    1. ...

    2. url이 about:blank라면 documentState의 origin을 documentState의 initiator origin으로 설정
    3. 그 외에 url이 about:srcdoc이라면 documentState의 origin을 상위의 active document의 origin으로 설정

    4. historyEntry를 새 세션 기록 항목으로 만들어 그 URL과 documentState를 설정합니다.
    5. fragment directive조각 지시문 제거 결과를 넣습니다.
    6. directive state지시문 상태(valuefragment directive)로 새로 생성합니다.

    7. historyEntry를 새 세션 기록 항목으로 만들어 URL은 url, documentState와 지시문 상태를 각각 설정합니다.

    8. navigationParams를 null로 설정

    9. ...

navigate to a fragment 정의에서:

HTML § 7.4.2.3.3 조각 탐색에 대한 몽키패치:

navigable navigable 및 ...이 주어졌을 때 조각으로 탐색:
  1. directive state를 navigable의 active 세션 기록 항목의 지시문 상태로 둡니다.

  2. fragment directive조각 지시문 제거의 결과를 넣습니다.

  3. fragment directive가 null이 아니면:

    조각만 바뀌고 지시문이 명시되지 않으면 현재 entry의 지시문 상태를 재사용하여 하이라이트 덮어쓰기를 방지합니다.
    1. directive state를 새로운 지시문 상태 (valuefragment directive)로 생성.

  4. historyEntry(새 세션 기록 항목) 작성:

    • URL url

    • document state navigable의 active 세션 기록 항목의 document state

    • scroll restoration mode navigable의 active 세션 기록 항목의 scroll restoration mode

    • 지시문 상태 directive state

  5. entryToReplace를 historyHandling이 "replace"면 navigable의 active 세션 기록 항목, 아니면 null로 둡니다.

  6. ...

URL 및 기록 업데이트 단계 정의에서:

HTML § 7.4.4 논-프래그먼트 동기 "탐색"에 대한 몽키패치:

Document document, ...이 주어졌을 때 "URL 및 기록 업데이트 단계":
  1. navigabledocument의 node navigable로 둡니다.

  2. activeEntrynavigable의 active 세션 기록 항목으로 둡니다.

  3. fragment directive조각 지시문 제거 결과로 둡니다.

  4. historyEntry(새 세션 기록 항목) 작성:

  5. document의 is initial about:blank가 true라면 historyHandling을 "replace"로 둡니다.

  6. historyHandling이 "push"라면:

    1. document의 history object index 증가

    2. document의 history object length를 index + 1로 설정

    3. newUrl이 activeEntry의 URL(프래그먼트 제외)와 다르거나 fragment directive가 null이 아니면:

      조각만 변경되고 지시문이 명시되지 않으면 active entry의 지시문 상태를 재사용합니다. 이것은 하이라이트 덮어쓰기를 방지합니다.
      1. historyEntry의 지시문 상태를 새로운 지시문 상태(value는 fragment directive)로 설정.

  7. 그 외 fragment directive가 null이 아니라면, historyEntry의 지시문 상태value를 fragment directive로 설정.

  8. serializedData가 null이 아니라면 document와 newEntry로 history object 상태 복원.

create navigation params by fetching 정의에서:

HTML § 7.4.5 세션 기록 항목 채우기에 대한 몽키패치:

세션 기록 항목 entry 등으로 navigation params by fetching 생성:
  1. 이 코드는 병렬로 실행됨을 보장합니다.

  2. ...

  3. currentURL을 request의 current URL로 설정
  4. commitEarlyHints를 null로 설정

  5. while true:

    1. request의 reserved client가 null이 아니고 currentURL의 origin과 reserved client 생성 URL의 origin이 다르면:

    2. ...

    3. currentURL을 locationURL로 설정
    4. fragment directive조각 지시문 제거(locationURL) 결과로 설정

    5. entry의 URL을 currentURL으로 설정
    6. entry의 URL을 locationURL로 설정

    7. entry의 지시문 상태valuefragment directive로 설정

    8. locationURL이 fetch scheme이 아닌 경우, request의 current URL의 origin을 initiator origin으로 써서 non-fetch scheme navigation params 새로 반환

    9. ...

Document는 히스토리 entry에서 채워지므로, 그 URL 에는 조각 지시문이 포함되지 않습니다. 또한 window의 Location 객체는 URL의 표현이므로 활성 문서에서 모든 getter는 조각 지시문이 제거된 URL 버전을 리턴합니다.

또한 HashChangeEvent 조각의 변경에 응답해 fired 되기 때문에, hashchange는 네비게이션 또는 traversal이 조각 지시문만 변경할 때는 fired 되지 않습니다.

다양한 edge case를 명확하게 하기 위한 예시가 아래에 제공됨

window.location = "https://example.com#page1:~:hello";
console.log(window.location.href); // 'https://example.com#page1'
console.log(window.location.hash); // '#page1'

초기 탐색으로 새 세션 기록 항목이 생성됩니다. 기록 항목의 URL은 조각 지시문이 제거된 "https://example.com#page1"입니다. 해당 entry의 지시문 상태 값은 "hello"가 됩니다. document가 entry에서 채워지므로, 웹 API는 URL에 조각 지시문을 포함하지 않습니다.

location.hash = "page2";
console.log(location.href); // 'https://example.com#page2'

동일 문서 탐색에서 조각만 바뀌었습니다. 이로 새 세션 기록 항목이 navigate to a fragment 단계에서 추가됩니다. 단, 조각만 바뀐 것이므로 새 entry의 지시문 상태는 첫 entry와 같은 상태("bar")를 참조합니다.

onhashchange = () => console.assert(false, "hashchange doesn’t fire.");
location.hash = "page2:~:world";
console.log(location.href); // 'https://example.com#page2'
onhashchange = null;

동일 문서 탐색에서 조각만 바뀌고, 조각 지시문도 포함되어 전달되었습니다. 명시적으로 지시문을 제공하면, 새 entry는 "fizz" 값을 가지는 별도의 지시문 상태가 됩니다.

페이지에 표시되는 fragment가 바뀌지 않았으므로 hashchange 이벤트는 fired 되지 않습니다. session history entry의 URL은 조각 지시문이 제거되어 비교가 이루어지기 때문입니다.

history.pushState("", "", "page3");
console.log(location.href); // 'https://example.com/page3'

pushState는 동일 문서에 대해 새 세션 기록 항목을 생성합니다. 비-프래그먼트 URL이 바뀌었으므로, 이 entry는 별도의 지시문 상태(null) 값을 가집니다.

URL이 세션 기록 entry에 설정되지 않는 다른 경우에는 조각 지시문 제거가 발생하지 않습니다.

URL 객체 예시:

let url = new URL('https://example.com#foo:~:bar');
console.log(url.href); // 'https://example.com#foo:~:bar'
console.log(url.hash); // '#foo:~:bar'

document.url = url;
console.log(document.url.href); // 'https://example.com#foo:~:bar'
console.log(document.url.hash); // '#foo:~:bar'

<a> 또는 <area> 요소:

<a id='anchor' href="https://example.com#foo:~:bar">Anchor</a>
<script>
  console.log(anchor.href); // 'https://example.com#foo:~:bar'
  console.log(anchor.hash); // '#foo:~:bar'
</script>

3.3.2. 문서에 지시문 적용

위 섹션에서는 조각 지시문이 URL에서 분리되어 세션 기록 항목에 저장되는 방식을 설명했습니다.

이 섹션에서는 탐색 및 이동(traversal)이 세션 기록 항목의 지시문 상태를 어떻게, 언제 사용하여 해당 세션 기록 항목에 연결된 지시문을 문서(Document)에 적용하는지 정의합니다.

Monkeypatching DOM § 4.5 문서 인터페이스:

각 문서는 보류 중인 텍스트 지시문(pending text directives)을 가집니다. 이 값은 null 또는 텍스트 지시문 리스트입니다. 초깃값은 null입니다.

히스토리 단계 적용을 위한 문서 업데이트의 정의에서:

Monkeypatching HTML § 7.4.6.2 문서 업데이트:

Document document 및 세션 기록 항목 entry ...이 주어졌을 때 문서 업데이트:
  1. ...

  2. document의 히스토리 객체 길이를 scriptHistoryLength로 설정
  3. 만약 documentsEntryChanged가 true라면:

    1. oldURLdocument의 latest entry의 URL로 둔다.

    2. document의 latest entry의 지시문 상태entry지시문 상태와 다르면:
      1. fragment directiveentry지시문 상태value로 둔다.

      2. document보류 중인 텍스트 지시문fragment directive 파싱 결과로 설정한다.

  4. document의 latest entry를 entry로 설정

  5. ...

3.3.3. 조각 지시문 문법

참고: 이 섹션은 비규범적입니다.

참고: 이 문법(grammer)은 참고용으로 제공되며, 파싱에 대한 규칙은 § 3.4 텍스트 지시문 섹션에 명령적으로 정의되어 있습니다. 만약 본 문법과 3.4 절의 단계가 불일치한다면 3.4절 내용이 권위(authoritative)합니다.

FragmentDirective는 "&" 문자로 분리된 여러 지시문을 포함할 수 있습니다. 이는 페이지 내 여러 문자열을 동시에 지시하기 위해 여러 텍스트 지시문을 허용함을 의미하지만, 앞으로 추가·결합되는 여러 지시문 유형(타입)을 위한 확장성을 위해 미지정(unknown) 지시문이 &로 분리된 목록에 있어도 파싱 실패하지 않습니다.

문자열(string)은 아래 EBNF(확장 배커스-나우어 형식)를 만족하면 유효한 조각 지시문입니다:

FragmentDirective ::=
(TextDirective | UnknownDirective) ("&" FragmentDirective)?
TextDirective ::=
"text="CharacterString
UnknownDirective ::=
CharacterString - TextDirective
CharacterString ::=
(ExplicitChar | PercentEncodedByte)*
ExplicitChar ::=
[a-zA-Z0-9] | "!" | "$" | "'" | "(" | ")" | "*" | "+" | "." | "/" | ":" | ";" | "=" | "?" | "@" | "_" | "~" | "," | "-"
ExplicitChar는 "&"를 제외한 모든 URL 코드포인트일 수 있습니다.

TextDirective는 아래 production을 만족해야 유효하다고 간주합니다:

ValidTextDirective ::=
"text=" TextDirectiveParameters
TextDirectiveParameters ::=
(TextDirectivePrefix ",")? TextDirectiveString ("," TextDirectiveString)? ("," TextDirectiveSuffix)?
TextDirectivePrefix ::=
TextDirectiveString"-"
TextDirectiveSuffix ::=
"-"TextDirectiveString
TextDirectiveString ::=
(TextDirectiveExplicitChar | PercentEncodedByte)+
TextDirectiveExplicitChar ::=
[a-zA-Z0-9] | "!" | "$" | "'" | "(" | ")" | "*" | "+" | "." | "/" | ":" | ";" | "=" | "?" | "@" | "_" | "~"
TextDirectiveExplicitCharURL 코드포인트FragmentDirective 또는 ValidTextDirective 구문에서 명시적으로 쓰이지 않은 쪽("&", "-", ",")만 제외합니다. 문서에서 "&", "-", ","가 필요한 경우 퍼센트 인코딩됩니다.
PercentEncodedByte ::=
"%" [a-zA-Z0-9][a-zA-Z0-9]

3.4. 텍스트 지시문

텍스트 지시문지시문의 한 종류로, 사용자에게 표시될 텍스트 범위를 나타냅니다. 네 개의 문자열(start, end, prefix, suffix)로 구성된 구조체(struct)입니다. start는 반드시 non-null이어야 하며, 나머지 세 항목은 null일 수 있습니다(제공되지 않은 경우). 네 항목 모두 빈 문자열은 허용되지 않습니다.

각 구성요소 의미와 사용 방식은 § 3.2 구문을 참고하십시오.

텍스트 지시문 용어 퍼센트 디코드 알고리즘(입력 문자열 term):
  1. term이 null이면 null 반환.

  2. Assert: termASCII 문자열입니다.

  3. decoded bytes퍼센트 디코딩 term 결과를 둔다.

  4. decoded bytesBOM 없는 UTF-8 디코딩을 실행한 결과를 반환.

텍스트 지시문 파싱 알고리즘(입력 문자열 text directive value):

본 알고리즘은 하나의 텍스트 지시문 문자열(e.g. "prefix-,foo,bar")을 받아 그 구성 요소(e.g. ("prefix", "foo", "bar", null))로 파싱합니다. 각 구성요소 의미와 사용 방식은 § 3.2 구문을 참고.

입력이 유효하지 않으면 null 반환, 유효하면 텍스트 지시문 반환.

  1. prefix, suffix, start, end를 모두 null로 둔다.

  2. Assert: text directive valueASCII 문자열이며 fragment percent-encode set의 코드포인트도 없고 U+0026 (&)도 없음.

  3. tokenslist로 하고, strictly split text directive value on U+002C(,).

  4. tokenssize가 1 미만 또는 4 초과면 null 반환.

  5. tokens[0]이 U+002D(-)로 끝나면:

    1. prefix부분 문자열(tokens[0]의 길이 - 1) 저장.

    2. tokens의 첫 번째 항목 제거.

    3. prefix가 빈 문자열이거나 U+002D(-)를 포함하면 null 반환.

    4. tokensempty면 null 반환.

  6. tokens 마지막 항목이 U+002D(-)로 시작하면:

    1. suffix부분 문자열(1 ~ 끝) 저장.

    2. tokens에서 마지막 항목 제거.

    3. suffix가 빈 문자열이거나 U+002D(-)를 포함하면 null 반환.

    4. tokensempty면 null 반환.

  7. tokens size가 2 초과면 null 반환.

  8. Assert: tokenssize는 1 또는 2.

  9. starttokens의 첫 항목 저장.

  10. 첫 항목 삭제.

  11. start가 빈 문자열이거나 U+002D(-)를 포함하면 null 반환.

  12. tokensempty가 아니면:

    1. end에 첫 항목 저장.

    2. end가 빈 문자열이거나 U+002D(-)를 포함하면 null 반환.

  13. 다음 값으로 새 텍스트 지시문 반환:

    prefix
    퍼센트 디코드 prefix
    start
    퍼센트 디코드 start
    end
    퍼센트 디코드 end
    suffix
    퍼센트 디코드 suffix

조각 지시문 파싱 알고리즘(입력 ASCII 문자열 fragment directive):

본 알고리즘은 조각 지시문 문자열(:~: 뒤 부분)을 받아 텍스트 지시문 리스트로 반환합니다. 빈 리스트도 반환 가능.
  1. directivesstrictly split fragment directive on U+0026(&).

  2. output을 빈 텍스트 지시문 리스트로 둔다.

  3. 각각의 문자열 directive에 대해:

    1. directive가 "text="로 시작하지 않으면 continue

    2. text directive value부분 문자열(인덱스 5 ~ 끝)을 저장합니다.

      Note: 이 값은 빈 문자열도 될 수 있음.
    3. parsed text directive파싱 text directive value 결과를 저장.

    4. parsed text directive가 null이 아니면 output 리스트에 추가

  4. output을 반환한다.

3.4.1. 텍스트 지시문 호출

이 섹션은 문서의 보류 중인 텍스트 지시문에 포함된 텍스트 지시문이 어떻게 처리되고, 관련 텍스트 구절의 강조(Indication)를 유발하는지 설명합니다.

이 섹션의 요약된 변경점:

지시된 부분에서, 프래그먼트가 range를 나타내도록 허용. 다음과 같이 변경:

Monkeypatching HTML § 7.4.6.3 조각으로 스크롤:

HTML 문서 document의 지시된 부분을 결정하기 위한 처리 모델은 다음과 같음:
  1. text directives를 document의 보류 중인 텍스트 지시문으로 설정.

  2. text directives가 null이 아니면:

    1. ranges리스트로 하고, 텍스트 지시문 호출 단계(text directives, document)를 실행한 결과로 둠.

    2. ranges가 비어 있지 않으면:

      1. firstRangeranges의 첫 아이템으로 둠.

      2. ranges의 각 range를 구현 정의 방식으로 시각적으로 표시. 이 표시는 author script에서는 관찰할 수 없어야 함. 자세한 사항은 § 3.7 텍스트 일치 표시 참고.

        ranges의 첫 range가 스크롤 대상이 되지만 모든 범위가 시각적으로 표시되어야 한다는 점 주의.
      3. firstRangedocument의 지시된 부분으로 설정하고, 반환.

  3. fragment를 document의 URL의 fragment로 둔다.

  4. fragment가 빈 문자열이면, 문서 맨 위(top of the document) 특수값을 반환.

  5. potentialIndicatedElement를 document와 fragment로 potential indicated element 찾기 결과로 둔다.

  6. ...

프래그먼트로 스크롤에서는 지시된 부분이 range일 수 있고, force-load-at-top 정책 적용 시 프래그먼트 스크롤을 방지하도록 처리. 다음과 같이 변경:

Monkeypatching HTML § 7.4.6.3 조각으로 스크롤:

  1. document의 indicated part가 null이면 document의 target element를 null로 설정.

  2. 그렇지 않고 indicated part가 문서 맨 위(top)이면:

    1. document의 target element를 null로 설정.

    2. 문서의 맨 처음으로 스크롤.

    3. return.

  3. 그 외:

    1. assertion: document의 indicated part는 element이거나 range임.

    2. scrollTarget을 document의 indicated part로 둠.

    3. targetscrollTarget을 복사.

    4. targetrange이면:

      1. targetfirst common ancestor(start node, end node)로 설정.

      2. target이 null이 아니면서 element가 아니면 target의 parent로 target 갱신하며 반복.

        만약 shadow tree 내부라면 target을 어떻게 잡아야 하는가?#190
    5. assertion: target은 element임.

    6. document의 target element를 target으로 설정.

    7. ancestor details revealing algorithm을 target으로 실행.

    8. ancestor hidden-until-found revealing algorithm을 target으로 실행.

      해당 알고리즘은 target이 ancestor나 루트 document node일 수도 있어 정상 동작하지 않을 수 있음. #89 참조. contain:style layout 블록으로 제한하면 해결될 수 있음.
    9. blockPosition은 scrollTarget이 range면 "center", 아니면 "start"로

      텍스트 지시문으로 이동 시 block flow에서 중앙에 맞춰짐(center).
    10. target을 "auto"/"start"/"nearest"로 into view.
    11. scroll a target into view, target = scrollTarget, behavior="auto", block=blockPosition, inline="nearest"

      구현체는 text directive에서 나온 target의 경우 스크롤을 생략할 수 있음.

    12. target에서 focusing steps 실행, 실패 시 문서 viewport fallback.

      구현 참고: Blink는 현 시점에서 텍스트 조각에 대해 포커스를 주지 않음. 개선 필요. TODO: crbug로 등록.
    13. 연속 포커스 네비게이션 시작 지점을 target으로 이동.

아래 두 개의 몽키패치는 fragment 검색 종료 시 보류 중인 텍스트 지시문이 반드시 null로처리됨을 보장합니다. 텍스트 지시문 탐색이 파싱 종료로 끝나면, 마지막으로 일반 프래그먼트도 재시도합니다.

try to scroll to the fragment 정의에서:

Monkeypatching HTML § 7.4.6.3 조각으로 스크롤:

Document document에 대해 fragment로 스크롤을 시도할 때, 병렬로 다음 단계 실행:
  1. 구현 정의 시간만큼 대기 (성능 최적화를 위한 user experience 확보 목적).

  2. document의 relevant global object로 navigation and traversal task source에 글로벌 task를 큐잉해서 다음 실행:

    1. document에 파서 없거나 파싱 중지, 혹은 스크롤 의도가 사라지도 abort.
    2. 사용자 에이전트가 더 이상 스크롤 의지가 없다고 판단하면:
      1. 보류 중인 텍스트 지시문을 null로.

      2. 이후 단계는 중단(abort).

    3. 문서에 파서가 없거나 파서가 중지된 경우:

      1. 보류 중인 텍스트 지시문이 null이 아니면:

        1. 보류 중인 텍스트 지시문을 null로.

        2. 프래그먼트로 스크롤(document) 실행.

      2. 이후 단계는 중단(abort).

    4. 문서에 대해 fragment로 스크롤 실행.

    5. indicated part가 여전히 null이면 fragment로 스크롤 재시도. else 보류 중인 텍스트 지시문을 null로.

navigate to a fragment 정의에서:

Monkeypatching HTML § 7.4.2.3.3 조각 탐색:

navigable navigable, ...로 조각 탐색 시:
  1. ...

  2. Update document for history step application(navigable의 active document, historyEntry, true, scriptHistoryIndex, scriptHistoryLength)
  3. navigable의 active document에 대해 fragment로 스크롤.

  4. navigable의 active document의 보류 중인 텍스트 지시문을 null로.
  5. traversable을 navigable의 traversable navigable로.

  6. ...

지시된 부분으로 스크롤하는 것은 "scroll to the fragment"에서 일어나는 여러 동작 중 하나일 뿐이다. 이에 따라, 관련 정의를 "조각 표시"로 리네임:

Monkeypatching HTML § 7.4.2.3.3 조각 탐색:

HTML § 7.4.2.3.3 조각 탐색 및 관련 단계를 모두 "조각 표시(indicating a fragment)"로 명칭 변경하여 더 포괄적인 효과를 반영.

3.5. 보안 및 프라이버시

3.5.1. 동기(motivation)

이 섹션은 비규범적입니다

텍스트 지시문을 구현할 때 교차 오리진 정보 유출에 사용되지 않도록 주의해야 합니다. 스크립트는 cross-origin URL에 텍스트 지시문을 포함하여 페이지를 탐색시킬 수 있습니다. 악의적 행위자가 해당 navigation 결과로 피해 페이지에서 텍스트 조각이 실제로 발견됐음을 알 수 있다면, 페이지의 특정 텍스트 존재 여부를 유추할 수 있습니다.

아래 하위 섹션의 처리 모델은 예상 공격 벡터를 완화하도록 제한합니다. 요약하면, 텍스트 지시문은 다음에 한정됨:

3.5.2. 탐색 시 스크롤

UA는 일치하는 텍스트 구절을 자동으로 시야로 스크롤해줄 수 있습니다. 사용성 측면에선 편리하지만 구현시 주의할 보안 리스크도 있습니다.

내비게이션 시 스크롤이 발생한 사실이 자연스러운 사용자 스크롤과 구분되어 감지될 가능성이 알려진 사례(혹은 잠재적 방법)가 있습니다.

대상 페이지의 iframe에 포함된 오리진이 IntersectionObserver로, 페이지 로드 후 500ms 내 스크롤 여부를 감지. 이 스크롤이 텍스트 조각 성공 여부를 알 수 있는 신호가 됨.
동일 네트워크의 두 사용자가 있다면, 공격자가 피해자에게 텍스트 조각이 포함된 링크 전송. 타깃 텍스트 부근에 희귀도메인 리소스를 배치하면, DNS 요청 순서로 fragment 일치 성공/실패 유추 가능.
공격자가 피해자에게 비공개 토큰 표시 페이지를 링크로 보냄. 피싱 방식 등으로 피해자가 토큰을 읽어보도록 유도하고, 텍스트 조각으로 인해 '기밀 유지 경고' 표시를 화면 밖으로 스크롤시킴.

이처럼 알려진 케이스는 타깃 페이지의 특수한 상황에서만 성립하므로 일반적으로 적용되는 건 아님. 텍스트 조각의 호출 조건을 더 제한하면 공격 여지가 더욱 좁아집니다. 다만, 여러 UA(브라우저)마다 위험 허용 기준이 다를 수 있으므로, 스크롤 동작 여부는 UA마다 개별적으로 고려해야 함.

표준 준수 UA는 내비게이션 시 자동 스크롤을 하지 않을 수도 있습니다. 대신 "클릭 시 스크롤" 같은 UI 혹은 아무런 동작도 제공하지 않을 수 있습니다. 이 경우에는 지시 대상 구절이 아래에 존재함을 사용자에게 알려주는 방식이 적합합니다.

위 예시에서 특정 상황에선 공격자가 페이지의 콘텐츠에 대해 1bit의 정보를 추출할 수 있지만, 반복공격으로 임의의 내용을 추출할 수 없도록 방어해야 함. 이를 위해 사용자 활성화 조건, 브라우징 컨텍스트 격리를 반드시 구현해야 함.

브라우징 컨텍스트 격리를 통해 다른 문서가 target 문서를 script로 조작할 수 없으므로 공격표면이 감소함.

또한, 해당 그룹 내 유일 브라우징 컨텍스트면(top level browsing context, 즉 전체 탭/윈도우)이므로, 악의적 사용 은폐도 어렵게 함.

UA가 자동 스크롤을 선택한다면, 반드시 문서가 백그라운드(비활성 탭 등)일 때는 스크롤이 일어나지 않게 해야 합니다. 이를 통해 비정상적 자동 탐색 시도를 사용자가 인지할 수 있고, 백그라운드 조작 공격도 방지할 수 있습니다.

UA가 자동 스크롤을 하지 않기로 할 경우, 텍스트 조각 일치 여부에 상관없이 제공된 fallback element-id는 반드시 시야로 스크롤해야 합니다. 스크롤 여부만으로 텍스트 조각 일치 여부가 감지되는 것을 막기 위함입니다.

3.5.3. 검색 타이밍

텍스트 검색 알고리즘을 단순히 구현하면 매칭/비매칭 쿼리의 실행시간 차이로 정보 유출이 가능해질 수 있습니다. 공격자가 텍스트 지시문 URL로 동기 탐색을 유도해 내비게이션 콜의 소요시간 차이로 텍스트 존재 여부를 파악할 수 있습니다.

§ 3.5.4 텍스트 조각 제한의 제약으로(특히 same-document-navigation 금지) 해당 케이스는 차단됨. 하지만 이런 제약은 여러 방어층 중 일부에 불과함.

즉, UA 구현체는 § 3.6 텍스트 조각으로의 탐색 각 단계의 runtime이 매칭 성공/실패에 따라 달라지지 않도록 보장해야 함.

구체적 방법은 본 명세에서 규정하지 않음. 예를 들어 UA는 텍스트 지시문 범위 찾기 함수에서 일치 후에도 계속 트리를 순회하거나, 또는 비동기 태스크로 찾고 Document의 indicated part를 나중에 설정하는 등의 방법이 있음.

3.5.4. 텍스트 조각 제한

이 섹션은 HTML 내비게이션과 통합되어 표시되는 텍스트 지시문의 스크롤 허용 시점을 제한합니다. 요약:

requestDocument 정의에 새로운 불리언 text directive user activation 필드를 포함:

Monkeypatching [FETCH]:

request는 관련 불리언 text directive user activation을 가집니다. 기본값은 false입니다.

Monkeypatching [HTML]:

Documenttext directive user activation이라는 불리언을 가집니다. 초깃값은 false입니다.

text directive user activation은 텍스트 조각의 단일 활성화를 허용하는 사용자 제스처 신호를 제공합니다. 문서가 로드될 때 내비게이션이 사용자 활성화로 인해 발생한 경우 true로 설정되며, 클라이언트 측 리디렉션에서도 전달됩니다.

만약 Documenttext directive user activation이 텍스트 조각 활성화에 사용되지 않으면, 대신 새 내비게이션 requesttext directive user activation을 true로 설정하는 데 사용됩니다. 이 방식으로 text directive user activation은 한 내비게이션에서 다른 내비게이션으로 전달될 수 있습니다.

Documenttext directive user activationrequesttext directive user activation은 사용 시 항상 false로 설정되어, 단일 사용자 활성화로 두 개 이상의 텍스트 조각을 활성화할 수 없습니다.

이 메커니즘은 텍스트 조각이 많은 웹 사이트에서 사용하는 일반적인 리디렉션을 통해 활성화될 수 있도록 합니다. 이런 사이트는 200 status code와 함께 window.location 을 설정하는 스크립트를 반환하여 사용자를 의도한 목적지로 리디렉션합니다.

실제 HTTP ( status 3xx ) 리디렉션과 달리, 이러한 "클라이언트 측" 리디렉션은 내비게이션이 사용자 제스처 결과임을 전달할 수 없습니다. text directive user activation 메커니즘은 이 경우에 한정된 user-activation의 전달을 가능하게 합니다. 이는 페이지가 프로그래밍적으로 텍스트 조각에 한 번 한정해 사용자 제스처처럼 내비게이션하게 합니다. 그러나 text fragment user activation은 초기화되므로 추가적인 텍스트 조각 내비게이션은 새 사용자 제스처 없이는 불가능합니다.

아래 다이어그램은 클라이언트 측 리디렉션을 통한 텍스트 조각 활성화에 flag가 어떻게 쓰이는지 보여줍니다:

Diagram showing how a text fragment flag is set and used

보다 심층적인 논의는 redirects.md 참고.

create navigation params by fetching 단계에서 active documenttext directive user activation 값을 requesttext directive user activation으로 전달.

Monkeypatching [HTML]:

  1. 이 코드는 병렬로 실행됩니다.

  2. documentResource를 entry의 document state의 resource로 둡니다.

  3. request를 새 request로 생성, 다음으로 설정

    url
    entry의 URL
    ...
    ...
    referrer policy
    entry의 document state의 request referrer policy
    text directive user activation
    navigableactive documenttext directive user activation
  4. navigableactive documenttext directive user activation을 false로 설정.

  5. documentResource가 POST resource이면:

    1. ...

navigation params 정의에 새 필드 추가:

Monkeypatching [HTML]:

user involvement
user navigation involvement 값.

navigation params가 어디서든 생성되는 곳에서 user involvement 값 초기화. 구체적으로는 create navigation params by fetching 케이스에서 true로 초기화:

Monkeypatching [HTML]:

To create navigation params by fetching given a session history entry entry, a navigable navigable, a source snapshot params sourceSnapshotParams, a target snapshot params targetSnapshotParams, a string cspNavigationType, a navigation ID-or-null navigationId, a NavigationTimingType navTimingType, 그리고 user navigation involvement user involvement, 다음 단계를 진행. navigation params, non-fetch scheme navigation params 또는 null을 반환
  1. 이 코드는 병렬로 실행됨.

  2. ...

  3. resultPolicyContainer를 response의 URL, entry의 document state의 history policy container, sourceSnapshotParams의 source policy container, null, responsePolicyContainer로 결정
  4. navigable의 컨테이너가 iframe이고 response의 timing allow passed flag가 설정된 경우 컨테이너의 pending resource-timing start time을 null로.

  5. navigation params를 새로 생성하여 반환:

    id
    navigationId
    ...
    ...
    about base URL
    entry의 document state의 about base URL
    user involvement
    user involvement

create and initialize a Document object 단계를 수정하여 text directive user activation 플래그를 계산 및 저장:

Monkeypatching [HTML]:

  1. navigationParams의 response, "pre-media"로 document에 link headers를 처리.

  2. documenttext directive user activation을 다음 조건 중 하나라도 만족하면 true, 아니면 false로 설정:
  3. document 반환

text directive allowing MIME typeMIME type인데, essence가 "text/html" 또는 "text/plain"입니다.

참고: scrolling to a fragment, fragment processing은 MIME type마다 별도 정의됨. 이 때문에 scroll to the fragment 단계에서 텍스트 지시문 스크롤 처리는 text/html에서만 적용되어야 함. 다만 실 브라우저는 HTML fragment 처리 로직을 text/plain에도 적용하는 경향이 있음(ID 가진 element 추가시 text/plain에서도 fragment-id 스크롤 동작 등). 실질적으로 text/plain에서 텍스트 지시문 활성화도 유용함. 기타 타입은 XS-Search 등 민감 데이터 공격 가능성 때문에 명확히 금지됩니다(ex: text/css, application/json, application/javascript 등).

이 내용이 HTML 명세에 쓰여도 되는가?

텍스트 지시문 스크롤 허용 체크 알고리즘: Document document, origin-or-null initiator origin, user navigation involvement-or-null user involvement를 인자로 아래 순서대로 수행:
  1. document보류 중인 텍스트 지시문이 null이거나 empty이면 false 반환.

  2. is user involved를 true로 둠: documenttext directive user activation이 true이거나, user involvement가 "activation" 또는 "browser UI"일 때. 아니면 false.

  3. documenttext directive user activation을 false로.

  4. documentcontent typetext directive allowing MIME type이 아니면 false 반환.

  5. user involvement가 "browser UI"면 true 반환.

    내비게이션이 브라우저 UI에서 시작되면 무조건 허용. 사용자가 직접 트리거한 것이고 페이지/스크립트가 텍스트 스니펫을 제공하지 않음.

    본 항목의 취지는 앱/페이지 측에서 URL을 제어할 수 있는 경우와 사용자가 완전히 제어하는 경우를 구분하기 위함. 전자는 스크립트가 텍스트 조각과 부작용(사이드이펙트) 모두 갖기에 cross-origin 시 그룹 분리된 브라우징 컨텍스트에서만 허용 필요. 새 창으로 열기 등 일부 browser UI 동작은 부분적으로 예외일 수 있음.

    관련 논의는 sec-fetch-site[FETCH-METADATA] 참고.

  6. is user involved가 false면 false 반환.

  7. documentnode navigableparent가 있으면 false 반환.

  8. initiator origin이 non-null이고 documentoriginsame origin이면 true 반환.

  9. documentbrowsing contextgroupbrowsing context set 길이가 1이면 true 반환.

    즉, cross-origin element/script에서 시작될 때는 문서가 noopener 컨텍스트(새 top level browsing context group, script 접근없는 새 프로세스)로 열렸을 때만 허용.
  10. 아니면 false 반환.

scroll to the fragment 단계를 수정하여 불리언 allow text directive scroll 파라미터 추가:

Monkeypatching HTML § 7.4.6.3 조각으로 스크롤:

Document document 및 불리언 allow text directive scroll를 인자로 fragment로 스크롤할 때:
  1. document의 indicated part가 null이면 target element를 null로.

  2. ...

  3. 그 외:

    1. assertion: indicated part는 element이거나 range임.

    2. ...

    3. target이 range라면:
      1. allow text directive scroll이 false면 return.

      2. target을 first common ancestor(start node, end node)로.

      3. ...

try to scroll to the fragment에 불리언 allow text directive scroll 플래그를 추가하고, step 2에서 큐잉되는 태스크 내용을 대체:

Monkeypatching [HTML]:

Document document, 불리언 allow text directive scroll으로 fragment로 스크롤 시 병렬로 다음 단계:
  1. 구현 정의 시간만큼 대기(사용자 경험 최적화 목적).

  2. document의 relevant global object로 navigation and traversal task 소스에 글로벌 태스크 큐잉, 다음 단계 수행:

    1. parser가 없거나, 파서가 중단되었거나, 스크롤을 원치 않는 상황이면 중단.

    2. fragment로 스크롤(document, allow text directive scroll).

    3. indicated part가 여전히 null이면 fragment로 스크롤(document, allow text directive scroll) 재시도.

update document for history step application 단계에 불리언 allow text directive scroll 파라미터를 받아 fragment로 스크롤 시 사용:

Monkeypatching [HTML]:

Document document, session history entry entry, 불리언 doNotReactivate, 정수 scriptHistoryLength, 정수 scriptHistoryIndex, (optional) session history entry 리스트 entriesForNavigationAPI, 불리언 allow text directive scroll 인자로 history step 적용:
  1. documentIsNew를 document의 latest entry가 null이면 true, 아니면 false로.

  2. ...

  3. documentsEntryChanged가 true면:
    1. oldURL을 document의 latest entry의 URL로.

    2. ...

  4. documentIsNew가 true라면:

    1. fragment로 스크롤(document, allow text directive scroll).

apply the history step 알고리즘에 불리언 allow text directive scroll를 받아, update document for history step application 호출 시 전달:

Monkeypatching [HTML]:

apply the history step를 step, traversable, 불리언 checkForCancelation, sourceSnapshotParams-or-null sourceSnapshotParams, navigable-or-null initiatorToCheck, user navigation involvement-or-null userInvolvementForNavigateEvents, 불리언 allow text directive scroll (기본값 false)와 함께 호출 시 아래 단계. 리턴값: "initiator-disallowed", "canceled-by-beforeunload", "canceled-by-navigate", "applied".

  1. completedChangeJobs ≠ totalChangeJobs 동안 반복:

    1. ...

    2. navigable의 active window로 글로벌 태스크 큐잉, 단계:
      1. changingNavigableContinuation의 update-only가 false면:

        1. ...

        2. history entry targetEntry를 activiate.

      2. updateDocument를 update document for history step application(targetEntry의 document, targetEntry, changingNavigableContinuation의 update-only, scriptHistoryLength, scriptHistoryIndex, entriesForNavigationAPI, allow text directive scroll)로 수행.

      3. targetEntry의 document = displayedDocument라면 updateDocument 수행.

  2. totalNonchangingJobs를 nonchangingNavigablesThatStillNeedUpdates의 크기로.

apply the push/replace history step에서 allow text directive scrolling를 받아 apply the history step에 전달:

Monkeypatching [HTML]:

apply the push/replace history step를 step, traversable traversable, 불리언 allow text directive scroll (기본값 false) 인자로 호출:

apply the history step(step, traversable, false, null, null, null, allow text directive scroll)의 결과를 반환.

참고: allow text directive scroll은 traversal, reload시 의도적으로 설정되지 않습니다. initiator origin, user involvement, history scroll state 체크를 모두 반복할 필요를 피하고, history scroll state가 더 중요하기 때문입니다. 이 경우에도 지시문의 하이라이트 복원은 계속 가능합니다.

finalize a cross-document navigation을 수정하여 user involvement 파라미터를 받아 allow text directive scrolling을 계산해서 apply the push/replace history step에 전달:

Monkeypatching [HTML]:

finalize a cross-document navigation을 navigable, historyHandling, historyEntry, 그리고 user navigation involvement user involvement (기본값 "none") 인자로 호출:

  1. 이 코드는 navigable의 traversable navigable의 session history traversal queue에서 실행됨.

  2. ...

  3. allow text directive scroll텍스트 지시문 스크롤 허용 체크(historyEntry의 document, historyEntry의 document stateinitiator origin, user involvement)로 결정
  4. apply the push/replace history step targetStep(traversable, allow text directive scroll) 실행.

navigate 알고리즘을 수정하여 finalize a cross-document navigation 단계로 user involvement을 전달:

Monkeypatching [HTML]:

  1. ...

  2. . 병렬로 다음 단계 수행:
    1. ...

    2. . historyEntry의 document를 채우는 단계서, navigable, "navigate", sourceSnapshotParams, targetSnapshotParams, navigationId, navigationParams, cspNavigationType, allowPOST true, completionSteps를 아래로:
      1. navigable의 traversable로 session history traversal steps에 finalize a cross-document navigation(navigable, historyHandling, historyEntry, userInvolvement) 추가.

Navigate to a fragment 알고리즘을 수정하여 initiator origin을 받아, fragment로 스크롤 시 allow text directive scroll 플래그 전달:

Monkeypatching [HTML]:

navigate to a fragment(navigable, url, historyHandling, userInvolvement, navigationAPIState, navigationId, origin initiator origin):

  1. navigation을 navigable의 active window의 navigation API로.

  2. ...

  3. update document for history step application(navigable의 active document, historyEntry, true, scriptHistoryIndex, scriptHistoryLength)
  4. same-document navigation API entry 업데이트(navigation, historyEntry, historyHandling)

  5. allow text directive scroll텍스트 지시문 스크롤 허용 체크(navigable의 active document, initiator origin, userInvolvement)로 결정

  6. fragment로 스크롤(navigable의 active document, allow text directive scroll)

navigate 알고리즘을 수정하여 fragment navigation 수행 시 initiator origin도 전달하도록:

Monkeypatching [HTML]:

  1. url과 navigable의 active document로 replace 필요 여부 결정.

  2. 아래가 모두 true라면:

    • documentResource가 null

    • response가 null

    • url이 navigable의 active session history entry의 URL(프래그먼트 제외)와 동일

    • url의 fragment는 non-null

    then:

    1. navigate to a fragment(navigable, url, historyHandling, userInvolvement, navigationAPIState, navigationId, initiatorOriginSnapshot)

    2. navigation을 navigable의 active window의 navigation API로.

3.5.5. 로드 시 스크롤 제한

이 섹션은 force-load-at-top 정책이 새 문서를 불러올 때 텍스트 지시문을 포함하여 모든 종류의 스크롤을 방지하는 방법을 정의합니다.

force-load-at-top이 Navigation API와 어떻게 상호작용하는지 결정 필요. [Issue #WICG/scroll-to-text-fragment#242]

restore persisted state 절차를 수정하여, 스크롤 복원을 억제하는 새 불리언 파라미터를 받게 합니다:

Monkeypatching [HTML]:

restore persisted state를 session history entry entry , 그리고 불리언 suppressScrolling으로:
  1. entry의 scroll restoration mode가 "auto"이고, suppressScrolling이 false이며, entry의 document의 relevant global object의 navigation API의 suppress normal scroll restoration during ongoing navigation이 false이면, entry로 scroll position data 복원.

  2. ...

update document for history step application 절차를 수정하여, 새 문서에서 force-load-at-top 정책을 체크하고 설정 시 스크롤을 수행하지 않도록 합니다.

Monkeypatching [HTML]:

  1. ...

  2. document의 history object의 length를 scriptHistoryLength로 설정.
  3. scrollingBlockedInNewDocument해당 정책 값 조회(force-load-at-top, document) 결과로 둠.

  4. documentsEntryChanged가 true라면:

    1. oldURL을 document의 latest entry의 URL로 둠.

    2. ...

    3. documentIsNew가 false면:
      1. same-document navigation의 navigation API entry를 navigation, entry, "traverse"로 업데이트.

      2. popstate 이벤트 발생...

      3. entry suppressScrolling false로 restore persisted state 실행.

      4. oldURL의 fragment가 ...와 다르면

    4. 그 외,

      1. entriesForNavigationAPI가 제공됨을 assert.

      2. entry scrollingBlockedInNewDocument로 restore persisted state 실행.

      3. 새 문서용 navigation API entry를 navigation, entriesForNavigationAPI, entry로 초기화.

  5. documentIsNew가 true면:

    1. scrollingBlockedInNewDocument가 false면, document에 대해 fragment로 스크롤 시도.

    2. 이 시점에 새로 생성된 문서에 대해 스크립트가 실행될 수 있음.

  6. 그 외 documentsEntryChanged가 false이고 doNotReactivate가 false면:

    1. ...

텍스트 조각 명세는 HTML § 7.4.2.3.3 Fragment navigations의 개정을 제안합니다. 요약하면, 텍스트 지시문이 존재하고 문서 내에서 일치하는 경우, 텍스트 조각이 element 조각보다 우선적으로 지시된 부분이 됩니다. HTML 문서의 indicated part 처리 모델을 element가 아닌 range를 반환하도록 수정하여, 그 범위가 시야로 스크롤됩니다.
두 노드 nodeAnodeB최초 공통 조상을 찾으려면:
  1. commonAncestornodeA로 둡니다.

  2. commonAncestor가 null이 아니고 shadow-including inclusive ancestornodeB가 아니면 commonAncestorshadow-including parent로 업데이트.

  3. commonAncestor를 반환.

nodeshadow-including parent를 찾으려면:
  1. nodeshadow root이면 nodehost를 반환.

  2. 그 외엔 nodeparent를 반환.

3.6.1. 문서 내 범위 찾기

이 섹션은 전체 조각 지시문 문자열을 문서 내 Range 리스트로 변환하는 여러 알고리즘과 정의를 안내합니다.

고수준에서 조각 지시문 문자열을 예시처럼 받습니다:

text=prefix-,foo&unknown&text=bar,baz

이를 개별 텍스트 지시문으로 쪼갭니다:

text=prefix-,foo
text=bar,baz

각 텍스트 지시문마다 문서 내에서 해당 지시문 조건에 맞는 렌더드 텍스트의 첫 사례를 검색합니다. 각 검색은 서로 독립적이며, 몇 개 지시문이 주어졌거나 일치 결과에 따라 결과가 달라지지 않습니다.

지시문이 문서 내 텍스트와 일치하면, 해당 일치 범위를 나타내는 range를 반환합니다. invoke text directives 단계가 이 섹션의 상위 API이며, fragment 지시문 문자열에 지정된 순서대로 각 지시문 개별 매칭 단계를 돌려서 매칭된 range 리스트를 반환합니다.

일치하지 않은 지시문은 반환 리스트에 항목을 추가하지 않습니다.

텍스트 지시문 호출 알고리즘: 입력값 리스트 타입의 텍스트 지시문 text directivesDocument document에 대해:
이 알고리즘은 시각적으로 표시될 range 리스트를 반환하며, 첫 번째 range가(UA가 자동 스크롤하면) 시야에 표시됩니다.
  1. rangesempty 리스트로 둠.

  2. 각각의 텍스트 지시문 directive에 대해:

    1. find a range from a text directive(directive, document) 결과가 null이 아니면 ranges에 append.

  3. ranges를 반환.

텍스트 지시문으로 범위 찾기 알고리즘: 텍스트 지시문 parsedValuesDocument document 입력으로 아래 단계:
본 알고리즘은 파싱에 성공한 텍스트 지시문과 검색 대상 문서를 입력받아, 일치하는 텍스트 구절의 첫 사례에 대응되는 range를 반환합니다. 찾지 못하면 null을 반환.

end가 null이면 "정확 일치"이며 반환되는 range는 start와 문자열만 일치. end가 있으면 "범위 검색"으로, 반환 range는 start부터 end까지. 이하 모든 모드에서 "일치 텍스트"라 통칭.

prefix, suffix는 null 허용(해당 쪽 문맥 무시). 예: prefix가 null이면 앞쪽 무관.

매칭 텍스트와 prefix/suffix는 block 경계를 넘을 수 있으나, 파라미터 단위로는 block 내에서만 매칭합니다. 즉, 각각의 prefix, start, end, suffix는 모두 단일 block 내 텍스트에만 적용.
:~:text=The quick,lazy dog
은 아래에선 매칭에 실패:
<div>The<div> </div>quick brown fox</div>
<div>jumped over the lazy dog</div>

이유: "The quick"이 단일 블록 내에 없고, 사이에 블록 요소가 있으므로 일치되지 않음.

다음 예시에서는 일치:

<div>The quick brown fox</div>
<div>jumped over the lazy dog</div>
  1. searchRangerange로 생성, start는 (document, 0), end는 (document, documentlength)

  2. searchRange가 collapsed가 아닐 때까지:

    1. potentialMatch를 null로 둔다.

    2. parsedValuesprefix가 null이 아니면:

      1. prefixMatchfind a string in range (query=parsedValuesprefix, searchRange, wordStartBounded=true, wordEndBounded=false) 실행 결과로 설정

      2. prefixMatch가 null이면 null 반환.

      3. searchRange의 start를 prefixMatch의 boundary point after로 이동

      4. matchRange를 start=prefixMatch의 end, end=searchRange의 end로 하는 range로

      5. matchRange의 startnext non-whitespace position으로 이동

      6. matchRange가 collapsed이면 null 반환.

        prefixMatch end나, 후행 non-whitespace가 문서 끝이면 이 상황 발생.
      7. Assert: matchRange의 start nodeText 노드임.

        이제 matchRange의 start는 matched prefix 직후의 non-whitespace에 위치.
      8. mustEndAtWordBoundary를, parsedValues의 end가 null이 아니거나 parsedValues의 suffix가 null이면 true, 아니면 false.

      9. potentialMatchfind a string in range(parsedValuesstart, searchRange, matchRange, wordStartBounded=false, wordEndBounded=mustEndAtWordBoundary) 결과로 설정

      10. potentialMatch가 null이면 null 반환.

      11. potentialMatch의 start가 matchRange의 start와 다르면 계속

        이 경우 prefix는 맞지만 뒤가 원하는 텍스트가 아니라 다음 prefix를 다시 찾기 위해 continue.
    3. 그 외:

      1. mustEndAtWordBoundary를 parsedValues의 end가 null이 아니거나 parsedValues의 suffix가 null이면 true, 아니면 false.

      2. potentialMatchfind a string in range(parsedValuesstart, searchRange, searchRange, wordStartBounded=true, wordEndBounded=mustEndAtWordBoundary) 결과로 설정.

      3. potentialMatch가 null이면 null 반환.

      4. searchRange의 start를 potentialMatch의 boundary point after로 이동.

    4. rangeEndSearchRange를 start=potentialMatch의 end, end=searchRange의 end로 하는 range로 설정.

    5. rangeEndSearchRange가 collapsed가 아닐 때까지:

      1. parsedValues의 end가 null이 아니면:

        1. mustEndAtWordBoundary를 parsedValues의 suffix가 null이면 true, 아니면 false.

        2. endMatchfind a string in range(parsedValuesend, searchRange, rangeEndSearchRange, wordStartBounded=true, wordEndBounded=mustEndAtWordBoundary) 결과로

        3. endMatch가 null이면 null 반환.

        4. potentialMatch의 end를 endMatch의 end로 설정.

      2. Assert: potentialMatch는 null 아님, collapsed 아님, 정확히 매칭 텍스트 범위를 나타냄.

      3. parsedValues의 suffix 가 null이면 potentialMatch 반환.

      4. suffixRange를 start=potentialMatch의 end, end=searchRange의 end인 range로

      5. suffixRange의 startnext non-whitespace position으로 이동.

      6. suffixMatch를 find a string in range(parsedValues의 suffix, searchRange, suffixRange, wordStartBounded=false, wordEndBounded=true) 결과로

      7. suffixMatch가 null이면 null 반환.

        suffix가 문서 남은 부분에 없으면 일치 불가.
      8. suffixMatch의 start가 suffixRange의 start와 같으면 potentialMatch 반환.

      9. parsedValues의 end가 null이면 break;

        정확 일치에서 suffix가 안 맞으면 inner loop에서 break(outer는 계속). 범위검색이면 end가 맞는지 계속 진행.
      10. rangeEndSearchRange의 start를 potentialMatch의 end로 갱신.

        이 경우 시작 범위는 맞췄으나, 정확 종료 범위는 못 맞췄다는 뜻. inner loop 계속하며 rangeEnd 추가 매칭 검색.
    6. rangeEndSearchRange가 collapsed라면:

      1. Assert: parsedValues의 end가 null 아님

      2. null 반환

        이 경우는 범위 매칭에서만 발생. 만약 유효한 rangeEnd+suffix 쌍을 못 찾으면 null 반환.
  3. null 반환

테스트
범위 rangerange start다음 비공백 위치로 이동시키려면 다음 단계를 따릅니다:
  1. range가 collapsed되지 않은 동안:

    1. noderangestart node로 둡니다.

    2. offsetrangestart offset으로 둡니다.

    3. node검색 불가 서브트리의 일부이거나 node가시 텍스트 노드가 아니거나 offsetnodelength와 같으면:

      1. rangestart node그림자-포함 순서의 다음 노드로 설정합니다.

      2. rangestart offset을 0으로 설정합니다.

      3. 계속.

    4. node의 offset offset 위치에서 6자를 substring data로 가져와 "&nbsp;"와 같다면:

      1. rangestart offset에 6을 더합니다.

    5. 그 외에 substring data의 5자가 "&nbsp"라면:

      1. rangestart offset에 5를 더합니다.

    6. 그 외의 경우:

      1. cp코드 포인트로, nodedata에서 offset 위치 값으로 둡니다.

      2. cpWhite_Space 속성이 없다면, 리턴.

      3. rangestart offset에 1을 더합니다.

범위에서 문자열 찾기 알고리즘은 문자열 query, range searchRange, 불리언 wordStartBoundedwordEndBounded로 실행:
이 알고리즘은 searchRange 내에 완전히 포함되는 query 텍스트의 첫 사례를 나타내는 range를 반환하며, 필요에 따라 단어 경계에서 시작/끝하는 일치를 제한할 수 있습니다(자세한 내용은 § 3.6.2 단어 경계 참고). 없으면 null 반환.

본 알고리즘의 기본 전제는 블록 내의 모든 검색 가능 텍스트 노드를 검사하여 리스트로 수집함. 이 리스트를 하나의 문자열로 합쳐 그 안에서 검색을 수행. 노드 리스트를 이용해 오프셋 등 계산, 결국 range를 반환.

노드 수집은 블록 노드를 만나면 종료되며, 예시 트리에서:

<div>
  a<em>b</em>c<div>d</div>e
</div>

위 구조라면 "abc"에서 먼저 검색, 이어 "d"와 "e"에서 따로 검색됨.

즉, query는 단일 블록 컨테이너 안에서 연속(중단 없는) 텍스트만 일치 가능.

  1. searchRange가 collapsed 상태가 아닌 동안:

    1. curNode를 searchRange의 start node로 둠.

    2. curNode검색 불가 서브트리의 일부라면:

      1. searchRange의 start node를 그림자-포함 트리 순서상 curNode의 shadow-including descendant가 아닌 다음 노드로.

      2. searchRange의 start offset을 0으로.

      3. 계속.

    3. curNode가시 텍스트 노드가 아니라면:

      1. searchRange의 start node를 다음 그림자-포함 순서의, doctype이 아닌 노드로.

      2. searchRange의 start offset을 0으로.

      3. 계속.

    4. blockAncestor가장 근접한 블록 조상으로.

    5. textNodeListText 노드 리스트(빈값)로.

    6. curNode가 blockAncestor의 그림자-포함 자손이고, 경계 지점(curNode, 0)이 searchRange의 end 뒤가 아닐 때까지 반복:

      1. curNode가 블록 레이아웃이면 break.

      2. curNode가 검색 비가시 노드면:

        1. curNode를 curNode의 shadow-including descendant가 아닌 다음 그림자-포함 순서 노드로 이동.

        2. 계속.

      3. curNode가 가시 텍스트 노드textNodeList에 append.

      4. curNode를 shadow-including tree order의 다음 노드로 이동.

    7. 노드 리스트에서 범위 찾기(query, searchRange, textNodeList, wordStartBounded, wordEndBounded) 실행 결과가 null이 아니면 반환.

    8. curNode가 null이면 break.

    9. Assert: curNode가 searchRange의 start node 이후임.

    10. searchRange의 start경계 지점(curNode, 0)으로.

  2. null 반환.

노드가 검색 비가시 노드임은 해당 노드가 element이고 HTML 네임스페이스에 속하며 다음 조건 중 하나라도 만족할 때입니다:

  1. 계산 값display 속성값이 none일 때.

  2. 노드가 void로 직렬화되는 경우.

  3. 다음 타입에 해당하는 경우: HTMLIFrameElement, HTMLImageElement, HTMLMeterElement, HTMLObjectElement, HTMLProgressElement, HTMLStyleElement, HTMLScriptElement, HTMLVideoElement, HTMLAudioElement

  4. select 요소이고, multiple 특성을 가지지 않은 경우.

노드가 검색 불가 서브트리 일부임은, 자신 또는 그림자-포함 조상 중에 검색 비가시 노드가 있을 때입니다.

노드는 가시 텍스트 노드임이란, Text 노드이고, 계산 값 중 부모 요소의 parent element visibility 속성이 visible이고, 해당 노드가 렌더링중일 때입니다.

노드가 블록 레벨 display를 가짐이란, 해당 노드가 element이고 계산 값display 값이 block, table, flow-root, grid, flex, list-item 중 하나일 때입니다.

node가장 근접한 블록 조상을 찾으려면:
  1. curNodenode로 둡니다.

  2. curNode가 null이 아닐 동안

    1. curNodeText 노드가 아니면서 블록 레벨 display가 있으면 curNode 반환.

    2. 그 외엔 curNodecurNodeparent로 설정.

  3. nodenode documentdocument element 반환.

search string queryString, range searchRange, Text 노드 리스트 nodes, 불리언 wordStartBounded, wordEndBounded 입력시 노드 리스트에서 범위 찾기:
(옵션) 매칭된 텍스트가 단어 경계에서 시작/끝해야만 매칭. 예를 들어:
검색어 “range”는 “mountain range”엔 항상 매칭되지만,
  1. 단어 경계에서만 시작해야 하면 “color orange”에는 매칭되지 않음.

  2. 단어 경계에서만 끝나야 하면 “forest ranger”에는 매칭되지 않음.

자세한 내용 및 예시는 § 3.6.2 단어 경계 참고.

  1. searchBuffernodes의 data를 이어붙인 값으로 저장.

    data는 실제로는 DOM에 존재하는 텍스트이며, 명세상은 렌더링된 텍스트로 처리되어야 함(및 Range 변환 필요). [이슈 #WICG/scroll-to-text-fragment#98]

  2. searchStart를 0으로 둠.

  3. nodes의 첫 번째 항목이 searchRange의 start node라면 searchStart를 searchRange의 start offset으로 설정.

  4. startend경계 지점(null)으로 둠.

  5. matchIndex를 null로 둡니다.

  6. matchIndex가 null인 동안 반복

    1. searchBuffer에서 queryStringsearchStart부터 처음 등장하는 인덱스를 matchIndex로 설정합니다. 문자열 검색은 기본 레벨로, 즉 대소문자/악센트/음표 등 무시로, [UTS10] 참조.

      직관적으로, 대소문자 구분 없이 악센트, 변형 등을 무시합니다.
    2. matchIndex가 null이면 null 반환.

    3. endIx를 matchIndex + queryString의 길이로 둠.

      endIx는 매칭된 문자열의 마지막 문자 인덱스 + 1.
    4. start를 경계 지점으로, get boundary point at index(matchIndex, nodes, isEnd=false) 실행 결과로.

    5. end를 경계 지점으로, get boundary point at index(endIx, nodes, isEnd=true) 실행 결과로.

    6. wordStartBounded가 true이고, searchBuffer에서 matchIndex가 단어 경계가 아니면, start node의 언어를 locale로 사용; 또는 wordEndBounded가 true이고, matchIndex+queryString의 길이단어 경계가 아니면, end node의 언어를 locale로 사용:

      1. searchStart를 matchIndex + 1로 설정.

      2. matchIndex를 null로 설정.

  7. endInset을 0으로 둡니다.

  8. nodes 마지막 항목이 searchRange의 end node라면, endInset을 (searchRange의 end node의 length − searchRange의 end offset)으로.

    endInset은 마지막 노드의 마지막 위치에서 뒤로 되돌아간 오프셋(범위에 포함되지 않은 노드 부분 길이).
  9. matchIndex+queryString의 길이가 searchBuffer의 길이 − endInset 초과면 null 반환.

    매칭이 검색 범위 끝을 넘으면 null 반환.
  10. Assert: start, end는 searchRange 내 유효 경계점임.

  11. range(start, end)를 반환.

정수 indexText 노드 리스트 nodes, 불리언 isEnd 입력으로 인덱스에서 경계점 구하기:

위 알고리즘에서 concatenated string의 인덱스가 어느 노드/위치에 해당하는지 구함.

isEnd는 시작/종료 위치 구분. 종료 인덱스는 "하나 이후" 문자를 가리킴. 만약 매치가 노드 경계에서 종료되면 end offset은 다음 노드의 첫 위치가 아니라 해당 노드 내 마지막 위치여야 함.

  1. counted를 0으로 둠.

  2. nodes의 각 curNode에 대해:

    1. nodeEnd를 counted + curNode의 length로.

    2. isEnd가 true면 nodeEnd에 1을 추가.

    3. nodeEnd가 index를 초과하면:

      1. 경계점(curNode, index - counted) 반환.

    4. counted에 curNode의 length를 더합니다.

  3. null 반환.

3.6.2. 단어 경계

단어 경계로 일치를 제한하는 것은 교차 오리진 정보 누출을 완화하는 방법 중 하나입니다.
Intl.Segmenter는 유니코드 세분화(단어 세분화 포함) 명세 제안입니다. 명세 확정 시 이 알고리즘은 Intl.Segmenter API를 활용해 단어 경계 매칭을 개선할 수 있습니다.

단어 경계[UAX29]Unicode Text Segmentation § Word_Boundaries에서 정의됩니다. Unicode Text Segmentation § Default_Word_Boundaries는 기본 단어 경계 집합을 정의하지만, 명세상 언급된 것처럼 로케일에 따라 더 정교한 알고리즘을 사용해야 합니다.

사전(dict) 기반 단어 경계 구분은 분리 문자가 없는 로케일(언어)에서 특히 주의해야 합니다. 예컨대 영어에서는 스페이스(' ')로 단어를 구분하지만, 일본어에서는 단어 구분 문자가 없습니다. 이 경우와, 알파벳이 100자 미만인 언어의 경우 사전에 가능한 1글자 단어가 알파벳의 20%를 넘지 않아야 합니다.

로케일문자열로, [BCP47] 언어 태그, 또는 빈 문자열이 올 수 있습니다. 빈 문자열은 주언어 미상임을 뜻합니다.

하위 문자열이 로케일 startLocaleendLocale에서 모두 첫 글자 위치와 마지막 글자 다음 위치가 각각 단어 경계일 때, 문자열 text에서 이 하위 문자열을 단어 경계로 묶인(word bounded)이라고 합니다.

숫자 position문자열 text에서 로케일 locale 기준으로 단어 경계에 있으면, 즉 단어 경계가 position번째 코드 유닛 앞에 있거나, text 길이가 0보다 크고 position이 0 또는 길이와 같을 때 단어 경계에 있음이라 정의합니다.

직관적으로, 하위 문자열이 단어 경계로 묶인 상태란 단어의 중간에서 시작하거나 끝나지 않는 경우입니다.

구분 기호(예: 공백)가 있는 언어(영어 등)에선 비교적 직관적이나, 줄바꿈, 하이픈, 따옴표 등 세부 내용은 앞서 언급한 기술문서에서 다룹니다.

중국어/일본어/한국어 등 단어 구분 기호 없는 언어는, 주어진 로케일의 사전을 통해 유효 단어를 판단해야 합니다.

텍스트 조각은 매칭 용어와 주변 문맥이 합쳐졌을 때 단어 경계로 끊겨야만 일치하도록 제한됩니다. 예를 들어, prefix,start,suffix와 같은 정확 매칭에서는 "prefix+start+suffix"가 전체가 단어 경계로 묶여야만 일치합니다. 반면 prefix,start,end,suffix와 같은 범위 매칭에서는 "prefix+start""end+suffix" 모두 단어 경계여야만 일치합니다.

이는 제삼자가 일치하는 토큰 전체를 이미 알고 있어야 한다는 보안을 위한 조치입니다. start,end와 같은 범위 매칭이 두 용어 사이 내부에서 단어 경계를 요구하지 않는다면, 제삼자가 반복 공격으로 토큰(예: "Balance: 123,456 $" 같은)을 한 글자씩 맞춰내려 시도할 수 있기 때문입니다(예: prefix="Balance: ", end="$" 및 start 변화).

자세한 보안 해설은 Security Review Doc 참고

"mountain range"라는 하위 문자열은 "An impressive mountain range"안에서는 단어 경계로 묶이지만, "An impressive mountain ranger"에는 단어 경계로 묶이지 않습니다.
일본어 "ウィキペディアへようこそ"(위키백과에 오신 것을 환영합니다)에서 "ようこそ"(환영합니다)는 단어 경계로 간주되지만, "ようこ"는 해당하지 않습니다.

3.7. 텍스트 일치 표시

UA는 조각으로 스크롤 시도 단계 또는 다른 메커니즘을 통해 텍스트 조각을 시야에 보이게 스크롤할 수 있으나, 반드시 스크롤해야 할 의무는 없습니다.

UA는 매칭된 텍스트가 눈에 띄도록(예: 명도 대비가 큰 하이라이트 등) 시각적으로 표시하여, 사용자가 해당 텍스트 일치를 인식할 수 있도록 해야 합니다.

UA는 사용자가 일치 시각적 표시를 해제(dismiss)할 수 있는 방법을 제공해야 하며, 해제 시 더 이상 매칭된 텍스트가 표시되지 않게 해야 합니다.

시각 효과 및 동작 방식의 구체적인 형태는 UA 구현에 맡깁니다. 그러나, UA는 표시를 위해 Document의 selection과 같은 author script에서 관찰 가능한 방법을 써서는 안 됩니다. 이는 컨텐츠 유출 공격 벡터가 될 수 있습니다.

UA는 제공된 문맥 용어(context term)는 시각적으로 표시해서는 안 됩니다.

인디케이터는 문서 내용에 속하지 않으므로, UA는 인디케이터와 페이지 본문(내용)이 사용자의 관점에서 구분될 수 있도록 처리해야 합니다.

UA는 인디케이터가 처음 나타날 때 몇 번, 이것이 링크 페이지에서 온 UA 제공 기능임을 안내하는 도움말 프롬프트를 제공할 수 있습니다.

3.7.1. UA 기능의 URL

UA는 문서의 URL을 노출하는 다양한 기능(예: location bar, 북마크 저장 등. window.location 같은 프로그래밍적 API는 제외)을 제공합니다.

사용자 혼동 방지를 위해, UA는 이런 URL에 조각 지시문 포함 여부를 일관되게 처리해야 합니다. 본 절은 UA가 각 상황을 처리하는 기본 원칙 사례를 제시합니다.

동작 일관성 확보를 위한 지침일 뿐, cross-UA 호환에 직접적 영향은 없으므로 엄격한 준수 의무는 아닙니다.

구체적 동작 결정은 각 구현 UA에 맡기며, 일부 UA는 사용자가 기본값을 구성하거나 UI를 통해 조각 지시문 포함 여부를 선택하게 할 수도 있습니다.

또한, UA가 사용 경험 개선을 다양한 방식으로 실험하는 것도 의미 있습니다. 예: 사용자가 스크롤해 일치 구절이 화면에서 벗어나면, 표시 URL에서 text fragment를 생략할 수도 있을 것입니다.

일반 원칙은 시각적 인디케이터(하이라이트 등)가 표시 중일 때만 URL에 조각 지시문을 포함시킵니다. 사용자가 인디케이터를 해제(dismiss)하면 URL에서 조각 지시문도 제거되어야 합니다.

URL에 텍스트 조각이 포함됐으나 현재 페이지에서 일치를 찾지 못한 경우, UA는 노출되는 URL에서 이를 생략할 수 있습니다.

해당 페이지에서 찾을 수 없는 텍스트 조각도, 사용자가 생성 시점 이후 페이지가 바뀌었음을 보여주는 정보로 쓸 수 있습니다.

그러나 북마크에선 이런 정보가 거의 의미 없을 것입니다.

아래에 몇 가지 대표적인 예시를 들었습니다.

여기서는 "text fragment"와 "fragment directive"를 동의어로 사용합니다. 이후 다른 타입의 지시문이 추가된다면, UX는 각 지시문별로 별도 논의가 필요합니다.
3.7.1.1. 주소 표시줄

주소 표시줄의 URL은 시각적 인디케이터(하이라이트)가 표시되는 동안 텍스트 조각을 포함해야 합니다. 사용자가 인디케이터를 해제하면, 주소 표시줄 URL에서 조각 지시문도 제거해야 합니다.

문서 내에서 일치하는 결과가 없어도, text fragment는 주소 표시줄 URL에 표시하는 것이 권장됩니다.

3.7.1.2. 북마크

대부분의 UA는 사용자가 현재 페이지의 링크를 북마크로 저장하는 기능을 제공합니다.

새로 생성된 북마크는 기본적으로, 일치가 발견되고 시각 인디케이터가 해제되지 않은 경우에만 URL에 조각 지시문을 포함해야 합니다.

북마크에서 URL로 이동 시 보통 내비게이션과 동일하게 조각 지시문을 처리해야 합니다.

3.7.1.3. 공유

일부 UA는 사용자가 현재 페이지를 다른 앱 또는 메신저 등으로 공유할 수 있도록 기능을 제공합니다.

이 경우도, 일치가 발견되고 시각적 인디케이터가 해제되지 않았을 때만 조각 지시문이 포함된 URL을 전달해야 합니다.

3.8. 문서 정책 통합

이 명세는 configuration point로 이름이 "force-load-at-top"인 항목을 Document Policy에 정의합니다. 타입boolean이며 기본값false입니다.

활성화되면 이 정책은 모든 자동 스크롤-온-로드 기능—텍스트 프래그먼트, 요소 프래그먼트, 히스토리 스크롤 복원—을 비활성화합니다.
사용자가 https://example.com#:~:text=foo로 이동한다고 가정합니다. example.com 서버의 응답 헤더는 아래와 같습니다:
Document-Policy: force-load-at-top

페이지가 로드되면, "foo"를 포함한 요소가 지시된 부분으로 지정되고 document의 target element가 됩니다. 그러나 "foo"는 시야에 스크롤되지 않습니다.

이 정책의 프래그먼트 기반 스크롤 차단은 본 문서 scroll to the fragment 알고리즘 개정으로 § 3.6 텍스트 조각으로 이동 절에 명시됩니다.

히스토리 스크롤 복원 차단은 restore persisted state 단계에서 2번 이후 아래 새 단계를 추가해 정의됩니다:

  1. 이 "force-load-at-top" policy의 값 조회, 그 결과가 true라면 UA는 Document나 그 어떤 스크롤 가능한 영역에도 스크롤 위치를 복원하지 않아야 함.

3.9. 기능 감지

기능 감지를 위해, UA가 해당 기능을 지원하는 경우 document.fragmentDirective를 통해 노출되는 새로운 FragmentDirective 인터페이스 추가를 제안합니다.

[Exposed=Window]
interface FragmentDirective {
};

이 객체는 향후 텍스트 프래그먼트 혹은 기타 조각 지시문에 대한 추가 정보를 제공하는 데 사용될 수 있습니다.

4. 텍스트 조각 지시문 생성

이 섹션은 비규범적입니다.

이 절은 UA가 텍스트 지시문을 포함한 URL을 자동 생성하는 경우의 권장 사항을 포함합니다. 이 권장 사항은 엄격한 규범 사항은 아니지만, 생성된 URL이 최대한 안정적이고 활용도 높게 만들기 위함입니다.

4.1. 정확 일치 우선

매칭 텍스트는 "text=foo%20bar%20baz"처럼 정확 문자열을 활용할 수도 있고, "text=foo,bar"와 같이 범위로도 지정할 수 있습니다.

가능하다면 전체 문자열을 정확 일치로 지정하는 것이 좋습니다. 이렇게 하면, 목적지 페이지가 삭제되거나 변경되었을 때에도, URL 자체에서 원래 의도된 대상을 유추할 수 있습니다.

예를 들어 다음 문장을 인용하고자 https://en.wikipedia.org/wiki/History_of_computing 링크를 만들려 한다면:
The first recorded idea of using digital electronics for computing was the
1931 paper "The Use of Thyratrons for High Speed Automatic Counting of
Physical Phenomena" by C. E. Wynn-Williams.

범위 기반 매칭은 다음과 같습니다:

https://en.wikipedia.org/wiki/History_of_computing#:~:text=The%20first%20recorded,Williams

또는 전체 문장을 정확 일치로 인코딩할 수도 있습니다:

https://en.wikipedia.org/wiki/History_of_computing#:~:text=The%20first%20recorded%20idea%20of%20using%20digital%20electronics%20for%20computing%20was%20the%201931%20paper%20%22The%20Use%20of%20Thyratrons%20for%20High%20Speed%20Automatic%20Counting%20of%20Physical%20Phenomena%22%20by%20C.%20E.%20Wynn-Williams

범위 기반 매칭은 덜 안정적입니다. 예를 들어, 페이지에 "The first recorded"가 앞부분에 새로 등장하면, 링크가 원치 않는 스니펫을 가리킬 수 있습니다.

범위 기반 매칭은 의미적으로도 덜 유용합니다. 문장이 지워질 경우, 사용자는 무엇이 원래 타깃이었는지 알 수 없게 되지만, 정확 일치의 경우 검색된 텍스트(없을 시)를 보여줄 수 있습니다.

인용 텍스트가 지나치게 길어서 전체 문자열 인코딩이 지나치게 긴 URL을 만들 위험이 있을 때, 범위 기반 매칭이 유용할 수 있습니다.

텍스트 스니펫이 300자 미만이라면 정확 매칭 방식으로 인코딩을 권장합니다. 그 이상이라면 범위 기반 방식도 허용할 수 있습니다.

TODO: 위 300자 기준을 임의성이 덜한 방식으로 바꿀 수 있을지 고려 필요.

4.2. 필요할 때만 문맥 사용

문맥 용어(context terms)는 텍스트 지시문이 페이지 내 텍스트 스니펫을 명확히 구별하는 데 쓰입니다. 다만 사용이 잦거나, 특정 구조(단락 경계 등)에서는 URL의 안정성이 취약해질 수 있습니다. 즉, 원하는 문자열이 요소 경계에서 시작/끝난다면, 문맥이 인접 요소에 존재하는데, 이런 구조적 변경 시 지시문이 무효화될 수 있습니다.

아래와 같은 텍스트에 대해 URL을 만들고자 하면:
<div class="section">HEADER</div>
<div class="content">Text to quote</div>

아래처럼 텍스트 지시문을 구성할 수 있습니다:

text=HEADER-,Text%20to%20quote

그러나, 페이지가 바뀌어 모든 section 헤더 옆에 "[edit]" 링크가 붙는다면, 이 URL은 더 이상 유효하지 않게 됩니다.

텍스트 스니펫이 길고 유일하다면, UA는 불필요한 문맥 용어 추가를 피해야 합니다.

아래 중 하나에 해당할 때만 문맥을 추가하세요:

TODO: 숫자 기준도 임의성 줄일 방법 고민.

4.3. 조각 ID 필요 여부 결정

UA가 텍스트 지시문을 포함한 URL로 이동할 때, 텍스트 조각에 일치하지 않으면 일반 element-id 기반 프래그먼트(조각)로 스크롤링 fallback합니다(존재시).

문서 내 텍스트가 변경되어 텍스트 지시문이 무효화될 때를 대비한 fallback 용도로 유용합니다.

다음과 같이 문장을 인용하고자 한다면:
The earliest known tool for use in computation is the Sumerian abacus

텍스트가 등장하는 section도 명시하면, 문구가 변경·삭제되어도 사용자가 관련 섹션으로 이동하게 할 수 있습니다:

https://en.wikipedia.org/wiki/History_of_computing#Early_computation:~:text=The%20earliest%20known%20tool%20for%20use%20in%20computation%20is%20the%20Sumerian%20abacus

다만, fallback element-id 프래그먼트가 적절히 선정되었는지 주의해야 합니다:

사용자가 https://en.wikipedia.org/wiki/History_of_computing#Early_computation 으로 이동 후, 아래 Symbolic Computations section까지 스크롤. 거기서 텍스트 선택 후 URL 생성:
By the late 1960s, computer systems could perform symbolic algebraic
manipulations

현 페이지 URL이 https://en.wikipedia.org/wiki/History_of_computing#Early_computation 라 해도, fallback으로 #Early_computation을 쓰는 것은 부적절. 만일 위 문장이 수정·삭제된다면, 페이지가 #Early_computation 섹션에서 시작돼 사용자를 혼란시킬 수 있음.

UA가 적절한 fallback 조각을 신뢰성 있게 결정할 수 없다면, URL에서 fragment id를 빼는 것이 바람직합니다:

https://en.wikipedia.org/wiki/History_of_computing#:~:text=By%20the%20late%201960s,%20computer%20systems%20could%20perform%20symbolic%20algebraic%20manipulations

적합성

문서 규칙

적합성 요구 사항은 설명적 선언과 RFC 2119 용어의 조합으로 표현됩니다. 규범 영역에서 “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, “OPTIONAL” 같은 키워드는 RFC 2119에서 설명된 대로 해석해야 합니다. 단, 가독성을 위해 본 명세에서는 항상 대문자가 아닌 소문자로 표기될 수 있습니다.

이 명세의 모든 텍스트는 비규범적, 예시, 주석으로 명시된 부분을 제외하곤 규범적입니다. [RFC2119]

본 명세 내 예시는 일반적으로 “for example”로 시작하거나 class="example"와 같은 코드로 구분되어 있습니다. 예:

이것은 정보 제공용 예시입니다.

비규범적 note는 “Note”로 시작하며 class="note" 코드로 구분됩니다. 예:

Note, 이것은 정보 제공용 노트입니다.

테스트

이 명세의 테스트 관련 내용은 이와 같은 “Tests” 블록으로 문서화될 수 있습니다. 이런 블록 자체는 비규범적입니다.


색인

이 명세에서 정의된 용어

참조에서 정의된 용어

참고 문헌

규범 참고 문헌

[CSS-CASCADE-5]
Elika Etemad; Miriam Suzanne; Tab Atkins Jr.. CSS Cascading and Inheritance Level 5. URL: https://drafts.csswg.org/css-cascade-5/
[CSS-DISPLAY-3]
Elika Etemad; Tab Atkins Jr.. CSS Display Module Level 3. URL: https://drafts.csswg.org/css-display/
[CSS-DISPLAY-4]
CSS Display Module Level 4. Editor's Draft. URL: https://drafts.csswg.org/css-display-4/
[CSSOM-VIEW-1]
Simon Pieters. CSSOM View Module. URL: https://drafts.csswg.org/cssom-view/
[DOCUMENT-POLICY]
Ian Clelland. Document Policy. ED. URL: https://wicg.github.io/document-policy
[DOM]
Anne van Kesteren. DOM Standard. Living Standard. URL: https://dom.spec.whatwg.org/
[ENCODING]
Anne van Kesteren. Encoding Standard. Living Standard. URL: https://encoding.spec.whatwg.org/
[FETCH]
Anne van Kesteren. Fetch Standard. Living Standard. URL: https://fetch.spec.whatwg.org/
[HTML]
Anne van Kesteren; et al. HTML Standard. Living Standard. URL: https://html.spec.whatwg.org/multipage/
[INFRA]
Anne van Kesteren; Domenic Denicola. Infra Standard. Living Standard. URL: https://infra.spec.whatwg.org/
[MIMESNIFF]
Gordon P. Hemsley. MIME Sniffing Standard. Living Standard. URL: https://mimesniff.spec.whatwg.org/
[RFC2119]
S. Bradner. Key words for use in RFCs to Indicate Requirement Levels. March 1997. Best Current Practice. URL: https://datatracker.ietf.org/doc/html/rfc2119
[UAX29]
Josh Hadley. Unicode Text Segmentation. 2023년 8월 16일. Unicode Standard Annex #29. URL: https://www.unicode.org/reports/tr29/tr29-43.html
[URL]
Anne van Kesteren. URL Standard. Living Standard. URL: https://url.spec.whatwg.org/
[UTS10]
Ken Whistler; Markus Scherer. Unicode Collation Algorithm. 2023년 9월 5일. Unicode Technical Standard #10. URL: https://www.unicode.org/reports/tr10/tr10-49.html
[WEBIDL]
Edgar Chen; Timothy Gu. Web IDL Standard. Living Standard. URL: https://webidl.spec.whatwg.org/

비규범 참고 문헌

[BCP47]
A. Phillips, Ed.; M. Davis, Ed.. Tags for Identifying Languages. 2009년 9월. Best Current Practice. URL: https://www.rfc-editor.org/rfc/rfc5646
[FETCH-METADATA]
Mike West. Fetch Metadata Request Headers. WD. URL: https://w3c.github.io/webappsec-fetch-metadata/

IDL 목록

[Exposed=Window]
interface FragmentDirective {
};

partial interface Document {
    [SameObject] readonly attribute FragmentDirective fragmentDirective;
};

이슈 목록

TODO: 만약 URL의 fragment가 ':~:'(즉, 빈 지시문)로 끝나면 null을 반환합니다. 이는 명시적 지시문이 없는 URL로 처리(기존 것을 덮어쓰지 않게 회피)됩니다. 그러나 이 경우 빈 문자열을 반환해야 하지 않을까요? 이렇게 하면 페이지가 '#:~:'으로 명시적으로 이동/푸시 상태값 설정 시 지시문/하이라이트를 클리어할 수 있습니다.
shadow tree 내부일 경우 target을 무엇으로 설정해야 할까요? #190
해당 revealing 알고리즘들은 target이 ancestor이거나 문서 루트 노드일 수도 있어 잘 작동하지 않습니다. 이슈 #89에서 contain:style layout 블록으로 제한을 권장했습니다.
구현 참고: Blink는 현 시점에서 텍스트 프래그먼트에 포커스를 주지 않습니다. 개선 필요. TODO: crbug 등록.
완전히 정확하진 않음. 크롬은 same-origin initiator에 대해서는 허용함. 명세 보완 필요. [이슈 #WICG/scroll-to-text-fragment#240]
이 내용이 HTML 명세에 기입되어도 되는가?
force-load-at-top과 Navigation API의 상호작용 방식을 결정해야 합니다. [이슈 #WICG/scroll-to-text-fragment#242]
data는 여기서 DOM의 텍스트 데이터라 올바르지 않습니다. 이 알고리즘은 렌더된 텍스트를 대상으로 하고, 나중에 다시 DOM Range로 변환해야 합니다. [이슈 #WICG/scroll-to-text-fragment#98]