화요정규식 – 4주차 : 기울임 글꼴 마크다운

화요일의 정규식 4주차 문제는 기울임 글꼴을 만드는 마크다운(markdown) 문법입니다. 마크다운은 “간단한 마크업 언어의 일종으로 이메일 상에서 일반 텍스트로 문장을 표기하던 관례를 규칙으로 만든 문법(위키피디아 출처)”입니다. 예를 들어, “*강조*”는 “강조“로 “**굵게**”는 “굵게“처럼 표시됩니다. 4주차 문제는 이 중 기울임 글꼴로 표시되는 강조 문법을 찾아내는 것입니다.

왼쪽의 내용이 오른쪽으로 변하도록 정규식과 치환식을 작성하면 됩니다.

This text is not italic. This text is not italic.
*This text is italic.* <em>This text is italic.</em>
This text is *partially* italic This text is <em>partially</em> italic
This text has *two* *italic* bits This text has <em>two</em> <em>italic</em> bits
**bold text (not italic)** **bold text (not italic)**
**bold text with *italic* ** **bold text with <em>italic</em> **
**part bold,** *part italic* **part bold,** <em>part italic</em>
*italic text **with bold** * <em>italic text **with bold** </em>
*italic* **bold** *italic* **bold** <em>italic</em> **bold** <em>italic</em> **bold**
*invalid markdown (do not parse)** *invalid markdown (do not parse)**
random *asterisk random *asterisk

문법 힌트

이 문제는 검증(assertion) 문법을 알고 있어야 풀 수 있습니다. 검증은 주어진 패턴의 앞 또는 뒤에 검증 코드에 일치하는 내용이 오는지도 확인합니다. 예를 들어 [a-z]+라는 패턴은 “a부터 z까지의 문자로만 이루어진 문자열”이라는 뜻이지만 [a-z]+(?=\d)라는 패턴은 “a부터 z까지의 문자로만 이루어진 문자열, 단 바로 뒤에 숫자가 있어야 함”이라는 뜻이 됩니다. 여기서 사용된 (?=…)를 가리켜 Lookahead assertion이라고 부릅니다. 또한 (?=…)는 “있는 경우”를 확인하기 때문에 Positive를 앞에 붙여 Positive lookahead assertion이라고 부르며, 검증부는 캡쳐되지 않기 때문에 폭이 없다는 뜻으로 zero width를 붙여 부르기도 합니다.

Lookahead Assertion

검증식이 앞에 나온 패턴을 꾸밉니다. 다시 말해 말로 풀이하면 “바로 에 …가 있어야 함(또는 없어야 함)”이라는 의미가 됩니다. “있어야 한다”는 규칙은 positive lookahead assertion이라 부르며 (?=검증패턴)와 같이 사용합니다. 예를 들어 q(?=u)라고 작성하면 u가 뒤에 있는 q를 찾습니다. 주의할 점은 검증식에 해당하는 부분 즉, u는 캡쳐되지 않는다는 것입니다.

“없어야 한다”는 규칙은 negative lookahead assertion이라 부르며 (?!검증패턴)와 같이 사용합니다. 예를 들어, “bar가 뒤에 없는 foo”는 foo(?!bar)와 같이 작성합니다.

Lookahead assertion은 앞에 나온 패턴을 꾸미는 것이므로 “foo가 앞에 없는 bar”를 찾기 위해 (?!foo)bar 패턴을 사용하면 원하는 결과를 얻지 못할 수 있습니다. 이 같은 용도에는 Lookbehind Assertion을 사용하여야 합니다.

Lookbehind Assertion

검증식이 뒤에 나온 패턴을 꾸며 “바로 에 …가 있어야 함(또는 없어야 함)”이라는 의미가 됩니다. lookahead assertion과 마찬가지로 있어야 한다는 규칙은 positive, 없어야 한다는 규칙은 negative를 사용하는데 단, 패턴 문법은 (?<=검증패턴) 또는 (?<!검증패턴)이 됩니다. 예를 들어 “foo가 앞에 있는 bar”는 (?<=foo)bar로 찾을 수 있습니다.

아쉽지만 자바스크립트에서는 Lookbehind Assertion을 지원하지 않습니다. 사용해보면 올바르지 않은 정규식 문법이라는 오류를 볼 수 있습니다.

욕심많은 수량자

앞서 배운 수량자 중 *와 +는 일치할 수 있는 최대 개수를 정해놓지 않고 있습니다. 이러한 수량자는 기본값이 “일치하는 최대의 범위”를 찾도록 되어있습니다. 예를 들어, “a123a456a789″와 같이 작성한 후 패턴을 a.+a라고 작성하면 “a123a”가 아닌 “a123a456a”에 일치하게 됩니다. 이렇게 항상 “최대의 범위”를 찾도록 하는 성질을 가리켜 욕심이 많은(greedy) 성질이라 표현하기도 합니다.

이를 욕심이 없는(ungreedy) 수량자로 만들려면 * 또는 + 뒤에 물음표(?)를 추가하면 됩니다. 이 때는 “일치하는 최소의 범위”를 찾습니다. 앞의 예제에서 패턴을 a.+?a와 같이 작성하면 “a123a”가 일치할 것입니다.

해답 및 풀이

[toggle txt_show=”4주차 풀이 보기” txt_hide=”4주차 풀이 감추기”]해답
정규표현식 : /(^|[^\*])\*(?!\*)(.*?[^\*])\*(?!\*)/g
치환식 : $1<em>$2</em>

풀이
규칙을 보면 단순합니다. *내용*을 <em>내용</em>로 치환하면 되니까요. 단, 여기에는 함정이 있는데 내용을 감싼 두 개의 *는 반드시 한 개만 사용되어야 하며 두 개가 연속으로 사용될 수 없습니다. 따라서, “*내용*” “* 내 용 *”은 모두 유효한 규칙이지만 “**내용*”과 같이 사용할 수는 없습니다.

이 때 사용할 수 있는 문법이 Assertion 입니다. “연속적이지 않고 한 개만 사용되는 *”라는 말은 “앞 뒤로 *가 없는 *”라는 말로 바꿀 수 있습니다. 이를 앞서 배운 규칙으로 작성하면 다음과 같습니다.

(?<!\*)\*(?!\*)

그리고 이 사이에 내용을 추가하면 됩니다. 네 번째 패턴처럼 원하는 패턴이 여러 개 나올 수도 있으므로 내용에 해당하는 수량자는 욕심이 없도록 만들었습니다. 그리고 일치하는 모든 패턴을 찾도록 g변경자를 사용하면 다음과 같은 패턴을 얻을 수 있습니다.

/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g

이렇게 작성했으면 깔끔하게 해결될 수 있었겠지만 앞에서 말했다시피 자바스크립트에는 Lookbehind assertion이 없습니다. 따라서 “*가 앞에 없는 *”이라는 Lookbehind 규칙 대신 “*가 아닌 문자나 문자열 시작이 있는 *”로 규칙을 바꾸었습니다. 주의할 점은 Assertion과는 달리 수정한 규칙은 캡쳐가 되기 때문에 치환할 때도 고려해주어야 한다는 것입니다. 따라서 이를 반영한 정규표현식과 치환식은 해답에서 밝힌 것과 같습니다. 만약 Lookbehind Assertion이 사용 가능했더라면 치환식은 “<em>$1</em>”로 사용할 수 있었을 것입니다.
[/toggle]

Leave a Reply