Profile img

Hi, I'm who's behind Fedify, Hollo, BotKit, and this website, Hackers' Pub! My main account is at @hongminhee洪 民憙 (Hong Minhee) :nonbinary:.

Fedify, Hollo, BotKit, 그리고 보고 계신 이 사이트 Hackers' Pub을 만들고 있습니다. 제 메인 계정은: @hongminhee洪 民憙 (Hong Minhee) :nonbinary:.

FedifyHolloBotKit、そしてこのサイト、Hackers' Pubを作っています。私のメインアカウントは「@hongminhee洪 民憙 (Hong Minhee) :nonbinary:」に。

Website
hongminhee.org
GitHub
@dahlia
Hollo
@hongminhee@hollo.social
DEV
@hongminhee
velog
@hongminhee
Qiita
@hongminhee
Zenn
@hongminhee
Matrix
@hongminhee:matrix.org
X
@hongminhee
1

Hongdown 0.2.0 is out! Hongdown is an opinionated formatter written in , and this release brings support, so you can now use it as a library in .js, , , and browsers.

New features:

  • Smart heading sentence case conversion with ~450 built-in proper nouns
  • SmartyPants-style typographic punctuation ("straight"“curly”)
  • External code formatter integration for code blocks
  • Directory argument support for batch formatting

Try it in the browser: https://dahlia.github.io/hongdown/

Release notes: https://github.com/dahlia/hongdown/discussions/10

1
7
1
2

jnkrtech replied to the below article:

Your CLI's completion should know what options you've already typed

洪 民憙 (Hong Minhee) @hongminhee@hackers.pub

Consider Git's -C option:

git -C /path/to/repo checkout <TAB>

When you hit Tab, Git completes branch names from /path/to/repo, not your current directory. The completion is context-aware—it depends on the value of another option.

Most CLI parsers can't do this. They treat each option in isolation, so completion for --branch has no way of knowing the --repo value. You end up with two unpleasant choices: either show completions for all possible branches across all repositories (useless), or give up on completion entirely for these options.

Optique 0.10.0 introduces a dependency system that solves this problem while preserving full type safety.

Static dependencies with or()

Optique already handles certain kinds of dependent options via the or() combinator:

import { flag, object, option, or, string } from "@optique/core";

const outputOptions = or(
  object({
    json: flag("--json"),
    pretty: flag("--pretty"),
  }),
  object({
    csv: flag("--csv"),
    delimiter: option("--delimiter", string()),
  }),
);

TypeScript knows that if json is true, you'll have a pretty field, and if csv is true, you'll have a delimiter field. The parser enforces this at runtime, and shell completion will suggest --pretty only when --json is present.

This works well when the valid combinations are known at definition time. But it can't handle cases where valid values depend on runtime input—like branch names that vary by repository.

Runtime dependencies

Common scenarios include:

  • A deployment CLI where --environment affects which services are available
  • A database tool where --connection affects which tables can be completed
  • A cloud CLI where --project affects which resources are shown

In each case, you can't know the valid values until you know what the user typed for the dependency option. Optique 0.10.0 introduces dependency() and derive() to handle exactly this.

The dependency system

The core idea is simple: mark one option as a dependency source, then create derived parsers that use its value.

import {
  choice,
  dependency,
  message,
  object,
  option,
  string,
} from "@optique/core";

function getRefsFromRepo(repoPath: string): string[] {
  // In real code, this would read from the Git repository
  return ["main", "develop", "feature/login"];
}

// Mark as a dependency source
const repoParser = dependency(string());

// Create a derived parser
const refParser = repoParser.derive({
  metavar: "REF",
  factory: (repoPath) => {
    const refs = getRefsFromRepo(repoPath);
    return choice(refs);
  },
  defaultValue: () => ".",
});

const parser = object({
  repo: option("--repo", repoParser, {
    description: message`Path to the repository`,
  }),
  ref: option("--ref", refParser, {
    description: message`Git reference`,
  }),
});

The factory function is where the dependency gets resolved. It receives the actual value the user provided for --repo and returns a parser that validates against refs from that specific repository.

Under the hood, Optique uses a three-phase parsing strategy:

  1. Parse all options in a first pass, collecting dependency values
  2. Call factory functions with the collected values to create concrete parsers
  3. Re-parse derived options using those dynamically created parsers

This means both validation and completion work correctly—if the user has already typed --repo /some/path, the --ref completion will show refs from that path.

Repository-aware completion with @optique/git

The @optique/git package provides async value parsers that read from Git repositories. Combined with the dependency system, you can build CLIs with repository-aware completion:

import {
  command,
  dependency,
  message,
  object,
  option,
  string,
} from "@optique/core";
import { gitBranch } from "@optique/git";

const repoParser = dependency(string());

const branchParser = repoParser.deriveAsync({
  metavar: "BRANCH",
  factory: (repoPath) => gitBranch({ dir: repoPath }),
  defaultValue: () => ".",
});

const checkout = command(
  "checkout",
  object({
    repo: option("--repo", repoParser, {
      description: message`Path to the repository`,
    }),
    branch: option("--branch", branchParser, {
      description: message`Branch to checkout`,
    }),
  }),
);

Now when you type my-cli checkout --repo /path/to/project --branch <TAB>, the completion will show branches from /path/to/project. The defaultValue of "." means that if --repo isn't specified, it falls back to the current directory.

Multiple dependencies

Sometimes a parser needs values from multiple options. The deriveFrom() function handles this:

import {
  choice,
  dependency,
  deriveFrom,
  message,
  object,
  option,
} from "@optique/core";

function getAvailableServices(env: string, region: string): string[] {
  return [`${env}-api-${region}`, `${env}-web-${region}`];
}

const envParser = dependency(choice(["dev", "staging", "prod"] as const));
const regionParser = dependency(choice(["us-east", "eu-west"] as const));

const serviceParser = deriveFrom({
  dependencies: [envParser, regionParser] as const,
  metavar: "SERVICE",
  factory: (env, region) => {
    const services = getAvailableServices(env, region);
    return choice(services);
  },
  defaultValues: () => ["dev", "us-east"] as const,
});

const parser = object({
  env: option("--env", envParser, {
    description: message`Deployment environment`,
  }),
  region: option("--region", regionParser, {
    description: message`Cloud region`,
  }),
  service: option("--service", serviceParser, {
    description: message`Service to deploy`,
  }),
});

The factory receives values in the same order as the dependency array. If some dependencies aren't provided, Optique uses the defaultValues.

Async support

Real-world dependency resolution often involves I/O—reading from Git repositories, querying APIs, accessing databases. Optique provides async variants for these cases:

import { dependency, string } from "@optique/core";
import { gitBranch } from "@optique/git";

const repoParser = dependency(string());

const branchParser = repoParser.deriveAsync({
  metavar: "BRANCH",
  factory: (repoPath) => gitBranch({ dir: repoPath }),
  defaultValue: () => ".",
});

The @optique/git package uses isomorphic-git under the hood, so gitBranch(), gitTag(), and gitRef() all work in both Node.js and Deno.

There's also deriveSync() for when you need to be explicit about synchronous behavior, and deriveFromAsync() for multiple async dependencies.

Wrapping up

The dependency system lets you build CLIs where options are aware of each other—not just for validation, but for shell completion too. You get type safety throughout: TypeScript knows the relationship between your dependency sources and derived parsers, and invalid combinations are caught at compile time.

This is particularly useful for tools that interact with external systems where the set of valid values isn't known until runtime. Git repositories, cloud providers, databases, container registries—anywhere the completion choices depend on context the user has already provided.

This feature will be available in Optique 0.10.0. To try the pre-release:

deno add jsr:@optique/core@0.10.0-dev.311

Or with npm:

npm install @optique/core@0.10.0-dev.311

See the documentation for more details.

Read more →
5
1

클로드 코드 스킬 잘쓰고싶은데 정말 모르겠다 ㅠㅠ…

  1. 클로드 코드 스킬이라는게 사실 agent랑 한 끗 차이라고 생각하기는 하는데 agent는 실행되면 눈에 보이는것과 달리 보이지도 않는다.
  2. 잘 발동도 안하는것 같길래, 무슨 히어로물에서 기술명 외치듯이 쓰다가...
  3. 이 방법 비슷하게도 사용해보고 있는데 잘안되는것 같기도하다.
  4. 클로드 코드 공식 문서에서 알게된 사실인데 커맨드 처럼 사용할 수 있기도해서 클로드 코드도 헷갈려하는 느낌이기도…

일단 심기 일전해서 description을 다시 재정비해봐야겠음.

2

클로드 코드 스킬 잘쓰고싶은데 정말 모르겠다 ㅠㅠ…

  1. 클로드 코드 스킬이라는게 사실 agent랑 한 끗 차이라고 생각하기는 하는데 agent는 실행되면 눈에 보이는것과 달리 보이지도 않는다.
  2. 잘 발동도 안하는것 같길래, 무슨 히어로물에서 기술명 외치듯이 쓰다가...
  3. 이 방법 비슷하게도 사용해보고 있는데 잘안되는것 같기도하다.
  4. 클로드 코드 공식 문서에서 알게된 사실인데 커맨드 처럼 사용할 수 있기도해서 클로드 코드도 헷갈려하는 느낌이기도…

일단 심기 일전해서 description을 다시 재정비해봐야겠음.

2

I've been working on a tricky problem in Optique (my CLI parser library): how do you make one option's value affect another option's validation and shell completion?

Think git -C <path> branch --delete <TAB>—the branch completions should come from the repo at <path>, not the current directory.

I think I've found a solution that fits naturally with Optique's architecture: declare dependencies between value parsers, then topologically sort them at parse time.

const cwdString = dependency(string());

const parser = object({
  cwd: optional(option("-C", cwdString)),
  branches: multiple(argument(
    cwdString.derive({
      metavar: "BRANCH",
      factory: dir => gitBranch({ dir }),
      defaultValue: () => process.cwd(),
    })
  )),
});

Details in the issue:

https://github.com/dahlia/optique/issues/74#issuecomment-3738381049

0

이번 주말+오늘 했던 약간의 야크셰이빙 공유

  1. vscode용 GUI git 확장을 구현하고 있다. (하는중)
  • Claude Code를 모든 팀 멤버가 사용하기로 결정하면서 기획문서도 일단은 git으로 관리하고 있는데 꽤나 재밌게 일하고 있다. 그런데 프로그래머가 아닌 멤버에게 vscode를 설치해서 마크다운 작성과 Claude Code 클라이언트를 사용 유도했던 것은 괜찮은 접근일 수 있었으나, 결국 좋은 git GUI 플러그인들은 돈주고 쓰긴해야해서 고민이 되었다.
  • 요즘 Remote desktop에 연결해서 주로 일을 하고 있는데, git kraken 같은 기존 강자(?)들도 remote에 ssh로 접속해서 하는등의 기능을 제공하지 않고 있다. workaround로 sshfs를 쓸 수 있으나 그 경우 git worktree를 사용하지 못하게됨.
  • 건너편 자리 동료가 Intelij에선 다되고 GUI로 하는게 CLI보다 빠르면서 실수도 적지 않느냐라는 얘기를 하면서 놀리는데, 어느정도는 맞는 얘기라고 생각하기도 한다.
  • 그래서 만들고 있다(!) 일단 맨날 쓰는 커맨드 위주로 만들고 있고 가장 중요한건 interactive rebase나 interactive add, split commit 같이 GUI에서 더 잘할 수 있는 일들까지 만드는게 목표.
  1. vscode로 kotlin +Spring 프로젝트 돌리다가, Kotlin 2.3.0 지원이 안되서 Language Runtime Server에 지원하도록 했다. (PR은 안 만들듯..)
  • https://github.com/fwcd/kotlin-language-server 은 꽤 오래부터 있던 라이브러리인데, 매번 vscode에서 이거 사용해가지고 kotlin + spring 서버 돌리려니까 실패를 했었다.
  • 오늘 Claude Code랑 같이 도전했더니 거의 성공했는데, kotlin-language-server가 kotlin 2.1.0을 지원하고있고, 우리 서비스는 2.3.0이라서 문제가 생긴다는 것을 발견했다.
  • 그래서 그냥 간단하게 2.3.0만 지원하도록 하려고했는데, java 버전도 25로 올라갔으므로 기존 19버전에서 25버전으로 같이 올렸다.
  • 별 패치는 없었지만 일단 잘 돌아간다.
  • 너무 큰 버전업이라서 올리기 어려운것도 있지만, JetBrain에서 드디어 공식 라이브러리를 만들고 있는 중이므로 잠깐 버티는 용도로만 써야겠다. https://github.com/Kotlin/kotlin-lsp
  • 참고로 우리 서비스는 gradle 멀티모듈을 사용하는데 이와 관련한 기능이 kotlin-lsp에서 지원되지 않기 때문에 사용할 수 가 없었다.
1
2
2

나는 CLI툴이 MCP보다 LLM에게 나은 도구라고 생각하는데, CLI 툴은 bash로 조합이 되기 때문이다. 즉 코딩이 가능하다. 디렉토리의 파일들의 각 첫 50줄을 읽는 작업은 ls, head, xargs를 조합해 한반의 호출로 가능하다. 그에 반해 MCP의 Read 툴 같은건호출을 파일 갯수만큼 해야한다.

이는 bash가 충분히 좋은 프로그래밍 언어라던가 MCP에 조합성을 추가할수가 없다는 얘기는 물론 아니다.

1
2
2

최근에 Manim-community 들어갔다가 GitHub에 있던 저장소가 Codeberg로 가게 된 걸 알게 되었는데 (지금은 또 GitHub에 돌아와있다) 그때 홈페이지에 How my GitHub Pages got Hacked 라는 글과 함께 해당 사실을 알리고 있었다[1]. 오늘 아마 이와 같은 내용으로 도메인을 뺏기는(?) 현상을 주변에서 봐서 verified domain들을 등록해놓았다.


  1. 당시 아카이브: https://web.archive.org/web/20251229233759/https://www.manim.community/ ↩︎

4

주말동안 Nix Flake의 워크스페이스 flakespace를 만들었는데, 구현이 너무 Hacky해서 솔직히 내가 만들어놓고도 쓸 맘이 안든다. 잡버그 고치느라 시간을 많이 버릴 걱정이든다. 일단 트라이는 해보겠다만.. Nix Flake가 자체적으로 워크스페이스 기능을 제공했으면 좋겠다.

3
9
7

이건 글 올라오기도 전에 그냥 결과물만 봐도 질려 버릴 정도였다. 도대체 본업 따로 있는 개인이 어떤 막강한 힘으로 이런 것을 완성 단계까지 만들어낸 것인지

  • https://dream-house.kr/ 내막을 전혀 모르고 그냥 이용 안내만 읽어도 대충 하는 집이 절대 아니라는 것이 느껴짐
  • https://github.com/segfault87/tasi653b-rs 음량계를 샀는데 윈도용 드라이버밖에 없길래 리눅스 시스템에 통합하기 위해 USB 드라이버 직접 짰다고 함. 야크가 남아나질 않음
  • https://github.com/segfault87/dxe
    • 그냥 디렉토리 트리 구조만 보고 있어도 흉내낼 엄두도 안 남. 전 세계에 과연 이런 합주실 시스템이 존재할까?
    • 하다못해 합주실 이용자들 주차 할인까지 자동화했다고? 여러 명이 하나의 예약으로 한꺼번에 방문한 경우까지 대응되어 있다고?
  • https://x.com/segfault87/status/1998019060592959756 ← ???

그야말로 전형적인 "생각은 해 볼 수 있는데 실현할 엄두는 나지 않는 것들"을 그냥 다 해 버린 케이스가 이렇게 가까운 곳에 있는 것이다.

20년 넘게 리눅스 삽질하던 사람이 작심하고 엔지니어링의 본때를 보여주겠다 하면 이렇게 되는 것인가. 이런 사람들과 같은 세계에 살며 뱁새가 황새 따라가는 것 이것 참 힘듭니다.

4

마스토돈 스타일의 새로운 커뮤 플랫폼, 커뮹! 모바일 앱이 출시되었어요! 베타 테스트에 참여해주신 여러분, 커뮹!에서 활동해 주시는 여러분 모두 응원해주셔서 감사합니다 🐓📲🥰

iOS: apps.apple.com/us/app/commung/
Android: play.google.com/store/apps/det

2
0
1
11

hi! i'm cat. i work on open source software and hardware, like:

and a lot more.

i joined mastodon.social back when it was cool, about a decade ago. it stopped being cool a while ago so i figured i'd move over to treehouse.social. here's to a decade more here!

3

Turns out writing markdownlint rules was the easy part. Actually fixing all those lint errors manually? That was unbearable.

So I did what any reasonable person would do: rewrote the whole thing as an auto-formatter. Meet Hongdown—now you can enforce my peculiar Markdown style without the pain.

https://github.com/dahlia/hongdown

0

One of the ways I'm dealing with AI slop at work is that when I'm giving feedback on the work I'm making sure to never assign the responsibility of the bad code to the AI. I'm directly saying that "this change that YOU made needs to be corrected". I'm always assigning the output of the AI to the person who put me in the position of reviewing the work. It is their responsibility to read the code that they're trying to review, they are responsible for 100% of the code, so they also get 100% of the blame when it's bad. If a change is confusing or nonsensical I'll ask "why did YOU make this change?". I'll never ask why an AI made a change, that we cannot know. All we can know is why someone thought it was acceptable to ship garbage, and we can assign them the responsibility for the garbage that they're willing to ship

2
0
0

모노레포를 쓸때 pnpm, cargo 등에 있는 워크스페이스를 많이들 쓴다. 근데 모노레포는 잠깐 제쳐두고, 워크스페이스가 뭐하는 기능일까? 워크스페이스는 여러개의 모듈을 동시에 작업할 때 쓰는 기능이다. 근데 어떤 모듈을 수정할 때 다른 모듈을 같이 수정해야 한다면, 걔들이 잘 정의된 모듈이 맞을까? 커플링이 있다면 그걸 제거하는게 정공법 아닌가?

여러 모듈을 동시에 고쳐야 하는 상황의 존재를 부정할 순 없다. 내 생각에 워크스페이스는 일시적으로 존재해야 하는 것이다. 사실 전혀 관계 없어 보이는 두 모듈을 어떤 뜬금없는 이유로 같이 고쳐야하는 경우도 종종 있다. 그럴때 잠깐 만났다 헤어지면 되는 것이다. 그러니까 워크스페이스는 서로 관계있는(관계 없어야 한다니깐) 모듈들이 천년만년 함께 모여있는 집이 아니라, 몇몇 모듈들이 잠깐 서로 용건이 생겼을 때 모이는 광장이어야 한다. 그런데 대부분의 경우 전자의 용례를 따르고 있다.

4

lat을 만들면서 클로드를 레포 4개에서 동시에 돌렸는데 예상치 못한 문제가 있었다. 내가 원래 모노레포를 별로 안 좋아해서(+ 레포를 나눴을때 빌드의 문제를 Nix가 해소해줌) 관련있는 레포 4개를 그냥 따로 따로 팠는데, 클로드가 일할때의 맥락을 전파해주는게 매우 번거로웠다. 클로드한테 줄 맥락을 기준으로 레포를 나누는게 맞는건가? 아니면 레포를 나누더라도 맥락을 다시 합칠 좋은 방법이 있을까?

2

I've always believed that structured logging shouldn't be complicated. Seeing Sentry echo this sentiment in their latest engineering blog post—and using LogTape to demonstrate it—is a massive validation for me.

They did a great job explaining why we need to move beyond console.log() in production. Really proud to see my work mentioned alongside such a standard-setting tool.

https://blog.sentry.io/trace-connected-structured-logging-with-logtape-and-sentry/

1
0
1
3
1

아 그리고, 진짜로 토큰 소비를 줄이고 LLM이 일하는데 도움이 되는지는 아직 모릅니다. 실험을 해봐야아는것이니 피드백 환영합니다.

2
6

🚀 새 오픈소스 프로젝트: hwplibsharp를 공개합니다!

.NET에서 HWP 파일을 다루고 싶었던 적 있으신가요? 저도 그랬습니다.

지난 3주간 Java 기반 hwplib를 .NET으로 완전히 포팅했습니다.

641개 파일, 50,000줄 이상의 코드, 54번의 커밋으로 .NET Standard 2.0, .NET Framework 4.7.2, .NET 8 이상을 지원하며, 크로스 플랫폼 (Windows, macOS, Linux)까지 고려한 것이 특징입니다.

AI 코딩 어시스턴트와 Visual Studio 2026의 에이전트 기반 디버거 덕분에 혼자서는 엄두도 못 냈을 규모의 프로젝트를 3주 만에 완료할 수 있었습니다.

특히 VS 2026의 디버거가 스스로 브레이크포인트를 설정하고, 변수를 추적하며, 문제의 근본 원인을 찾아주는 경험은 시니어 개발자와 페어 디버깅하는 느낌이었습니다.

이 프로젝트는 neolord0(박성균)님의 hwplib가 없었다면 불가능했습니다. 오픈소스 생태계는 이렇게 서로의 어깨 위에 서서 성장합니다.

https://devwrite.ai/ko/posts/hwplibsharp-dev-journey/

5
1

레포 파놨더니 모르는 사람들이 스타를 찍기 시작해서. 급하게 WIP라고 써놨다. 빨리 완성하자...

2

식탁보를 만들어감에 있어 좀 더 많은 분들께서 편하게 참여하실 수 있도록, Discord 서버를 새로 오픈했습니다. 많이 참여해주시고 알려주시면 감사하겠습니다!

https://discord.gg/eT2UnUXyTV

1

LLM 코딩의 부정적인 영향으로 우려하는 부분이 있는데, 점점 현실이 되어가는거 같다. 사람들이 코드를 안 읽고 너무 많이 짠다.

사실 오랫동안 많은곳에서 쓰이는 좋은 프로그램들은, 그 많은 코드를 짜서 어떻게든 동작시키게 만들었다는 것 이전에, 그냥 '뭘 만들지'에 대한 접근부터 훌륭한 경우가 많다(많은 UNIX 기본 프로그램들을 생각해보라). 옛날에는, 내가 어떤 기능 A가 필요할때, 내가 그 기능 A를 짜는게 힘든 일이니까 그 기능 A를 제공하는 프로그램을 먼저 찾게되고, 그러면 그 프로그램은 나의 근시안적인 접근보다 나은 더 좋은 개념을 갖고 있는 경우가 많았다. 그러면 나는 이제 그 개념을 익히며 내 자신을 업데이트하면 되는 것이다.

근데 자기 자신을 업데이트하는건 피곤한 일이라서, 사람들은 그걸 피할수 있는 기회를 놓치지 않는다. 그래서 현재 자기 수준에 맞는 평범한 프로그램을 만드는 길을(단돈 0.1$) 기꺼이 선택해버린다.

7
2
8
0
0

I finally gave in and wrote my own markdownlint rules to enforce my peculiar and stubborn Markdown style. Probably no one else will ever need these, but I've published them as open source anyway.

https://github.com/dahlia/markdownlint-rules

0

LLM을 위한 cat, lat을 만들어볼까 생각이 들었다. 어떤 파일을 열던지 간에, 토큰 아끼고 LLM이 쉽게 읽도록 적당히 알아서 바꿔서 보여주는 것이다. 그리고 CLAUDE.md 같은거에 cat 대신에 쓰라고 하는거지.

처음엔 JSON 파일을 minify해서 토큰 아끼는 정도를 생각했는데, 막상 클로드한테 물어보니 자기도 들여쓰기가 있어야 읽기 편하다고 한다. 응? 그래서 쓸모있는 접근을 물어봤더니, 코드를 읽을때 앞에 함수 시그니쳐/클래스 정의 등의 요약을 달아주면 좋겠다고 한다.

쓸모있게 만드려면 좀더 고민해야할듯..

2

LLM에서 마크다운이 널리 쓰이게 되면서 안 보고 싶어도 볼 수 밖에 없게 된 흔한 꼬라지로 그림에서 보는 것처럼 마크다운 강조 표시(**)가 그대로 노출되어 버리는 광경이 있다. 이 문제는 CommonMark의 고질적인 문제로, 한 10년 전쯤에 보고한 적도 있는데 지금까지 어떤 해결책도 제시되지 않은 채로 방치되어 있다.

문제의 상세는 이러하다. CommonMark는 마크다운을 표준화하는 과정에서 파싱의 복잡도를 제한하기 위해 연속된 구분자(delimiter run)라는 개념을 넣었는데, 연속된 구분자는 어느 방향에 있느냐에 따라서 왼편(left-flanking)과 오른편(right-flanking)이라는 속성을 가질 수 있다(왼편이자 오른편일 수도 있고, 둘 다 아닐 수도 있다). 이 규칙에 따르면 **는 왼편의 연속된 구분자로부터 시작해서 오른편의 연속된 구분자로 끝나야만 한다. 여기서 중요한 건 왼편인지 오른편인지를 판단하는 데 외부 맥락이 전혀 안 들어가고 주변의 몇 글자만 보고 바로 결정된다는 것인데, 이를테면 왼편의 연속된 구분자는 **<보통 글자> 꼴이거나 <공백>**<기호> 또는 <기호>**<기호> 꼴이어야 한다. ("보통 글자"란 공백이나 기호가 아닌 글자를 가리킨다.) 첫번째 꼴은 아무래도 **마크다운**은 같이 낱말 안에 끼어 들어가 있는 연속된 구분자를 허용하기 위한 것이고, 두번째/세번째 꼴은 이 **"마크다운"** 형식은 같이 기호 앞에 붙어 있는 연속된 구분자를 제한적으로 허용하기 위한 것이라 해석할 수 있겠다. 오른편도 방향만 다르고 똑같은 규칙을 가지는데, 이 규칙으로 **마크다운(Markdown)**은을 해석해 보면 뒷쪽 **의 앞에는 기호가 들어 있으므로 뒤에는 공백이나 기호가 나와야 하지만 보통 글자가 나왔으므로 오른편이 아니라고 해석되어 강조의 끝으로 처리되지 않는 것이다.

CommonMark 명세에서도 설명되어 있지만, 이 규칙의 원 의도는 **이런 **식으로** 중첩되어** 강조된 문법을 허용하기 위한 것이다. 강조를 한답시고 **이런 ** 식으로 공백을 강조 문법 안쪽에 끼워 넣는 일이 일반적으로는 없으므로, 이런 상황에서 공백에 인접한 강조 문법은 항상 특정 방향에만 올 수 있다고 선언하는 것으로 모호함을 해소하는 것이다. 허나 CJK 환경에서는 공백이 아예 없거나 공백이 있어도 한국어처럼 낱말 안에서 기호를 쓰는 경우가 드물지 않기 때문에, 이런 식으로 어느 연속된 구분자가 왼편인지 오른편인지 추론하는 데 한계가 있다는 것이다. 단순히 <보통 문자>**<기호>도 왼편으로 해석하는 식으로 해서 **마크다운(Markdown)**은 같은 걸 허용한다 하더라도, このような**[状況](...)**は 이런 상황은 어쩔 것인가? 내가 느끼기에는 중첩되어 강조된 문법의 효용은 제한적인 반면 이로 인해 생기는 CJK 환경에서의 불편함은 명확하다. 그리고 LLM은 CommonMark의 설계 의도 따위는 고려하지 않고 실제 사람들이 사용할 법한 식으로 마크다운을 쓰기 때문에, 사람들이 막연하게 가지고만 있던 이런 불편함이 그대로 표면화되어 버린 것이고 말이다.

* 21. Ba5# - 백이 룩과 퀸을 희생한 후, 퀸 대신 **비숍(Ba5)**이 결정적인 체크메이트를 성공시킵니다. 흑 킹이 탈출할 곳이 없으며, 백의 기물로 막을 수도 없습니다. [강조 처리된 "비숍(Ba5)" 앞뒤에 마크다운의 강조 표시 "**"가 그대로 노출되어 있다.]

As Markdown has become the standard for LLM outputs, we are now forced to witness a common and unsightly mess where Markdown emphasis markers (**) remain unrendered and exposed, as seen in the image. This is a chronic issue with the CommonMark specification---one that I once reported about ten years ago---but it has been left neglected without any solution to this day.

The technical details of the problem are as follows: In an effort to limit parsing complexity during the standardization process, CommonMark introduced the concept of "delimiter runs." These runs are assigned properties of being "left-flanking" or "right-flanking" (or both, or neither) depending on their position. According to these rules, a bolded segment must start with a left-flanking delimiter run and end with a right-flanking one. The crucial point is that whether a run is left- or right-flanking is determined solely by the immediate surrounding characters, without any consideration of the broader context. For instance, a left-flanking delimiter must be in the form of **<ordinary character>, <whitespace>**<punctuation>, or <punctuation>**<punctuation>. (Here, "ordinary character" refers to any character that is not whitespace or punctuation.) The first case is presumably intended to allow markers embedded within a word, like **마크다운**은, while the latter cases are meant to provide limited support for markers placed before punctuation, such as in 이 **"마크다운"** 형식은. The rules for right-flanking are identical, just in the opposite direction.

However, when you try to parse a string like **마크다운(Markdown)**은 using these rules, it fails because the closing ** is preceded by punctuation (a parenthesis) and it must be followed by whitespace or another punctuation mark to be considered right-flanking. Since it is followed by an ordinary letter (), it is not recognized as right-flanking and thus fails to close the emphasis.

As explained in the CommonMark spec, the original intent of this rule was to support nested emphasis, like **this **way** of nesting**. Since users typically don't insert spaces inside emphasis markers (e.g., **word **), the spec attempts to resolve ambiguity by declaring that markers adjacent to whitespace can only function in a specific direction. However, in CJK (Chinese, Japanese, Korean) environments, either spaces are completly absent or (as in Korean) punctuations are commonly used within a word. Consequently, there are clear limits to inferring whether a delimiter is left or right-flanking based on these rules. Even if we were to allow <ordinary character>**<punctuation> to be interpreted as left-flanking to accommodate cases like **마크다운(Markdown)**은, how would we handle something like このような**[状況](...)は**?

In my view, the utility of nested emphasis is marginal at best, while the frustration it causes in CJK environments is significant. Furthermore, because LLMs generate Markdown based on how people would actually use it---rather than strictly following the design intent of CommonMark---this latent inconvenience that users have long felt is now being brought directly to the surface.

* 21. Ba5# - 백이 룩과 퀸을 희생한 후, 퀸 대신 **비숍(Ba5)**이 결정적인 체크메이트를 성공시킵니다. 흑 킹이 탈출할 곳이 없으며, 백의 기물로 막을 수도 없습니다. [The emphasized portion `비숍(Ba5)` is surrounded by unrendered Markdown emphasis marks `**`.]
14
2
1
2