PlCodeBlock
한 줄짜리 코드부터 천 줄짜리까지 보여 주는 뷰어입니다. 위에는 바, 옆에는 줄 번호, 각 줄 앞에는 프롬프트, 그리고 읽을 팔레트 열두 벌이 있습니다.
import { PlCodeBlock } from 'plass-ui';
<PlCodeBlock code={source} language="tsx" title="src/Save.tsx" />;import 'package:plass_ui/plass_ui.dart';
PlCodeBlock(
code: source,
language: 'dart',
title: const Text('lib/save.dart'),
);코드 위에 그려지는 것은 전부 선택이고 각각 prop 하나로 켜고 끕니다. 같은 컴포넌트가 문장 안에 끼는 짧은 조각(바도, 번호도, 장식도 없는) 이면서 동시에 README 맨 위의 전체 기록이어야 하기 때문입니다.
Props
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| code * | string | — | 코드. 블록 끝의 공백은 잘라 냅니다 |
| language | string | — | 무엇으로 쓰였는지 — ts, dart, bash, yml. 흔한 표기와 확장자를 알아듣습니다 |
| theme | 'dark' | 'light' | 'auto' | 'mono' | (string & {}) | 'dark' | 팔레트. auto를 빼면 페이지의 명암과 무관합니다. 등록되지 않은 이름도 받습니다 |
| highlight | boolean | true | 코드에 색을 입힙니다. 끄면 문법 엔진을 아예 받아 오지 않습니다 |
| toolbar | boolean | true | 코드 위의 바. 끄면 showLanguage·copyable·rawToggle이 무엇을 말하든 아무것도 그리지 않습니다 |
| title | ReactNode | — | 바 앞쪽의 이름. 보통 파일 경로입니다 |
| showLanguage | boolean | true | 바에 언어 이름을 적습니다 |
| copyable | boolean | true | 코드를 클립보드에 올리는 버튼 |
| rawToggle | boolean | false | 색을 걷어 내고 문자 그대로 보여 주는 토글 |
| highlightLines | number | string | Array<number | string> | — | 표시할 줄. 4, '4-9', '1,4-9,12'. gutter가 세는 방식으로 셉니다 |
| lineNumbers | boolean | false | 옆에 줄 번호를 붙입니다 |
| startLine | number | 1 | 첫 줄의 번호 |
| prompt | string | — | 내용이 있는 모든 줄 앞의 셸 프롬프트 — $, #, >>>. 그려지지만 복사되지는 않습니다 |
| wrap | boolean | false | 긴 줄을 옆으로 흘리는 대신 접습니다 |
| maxHeight | number | string | — | 이 높이를 넘으면 안에서 스크롤합니다. 숫자는 픽셀 |
| fontFamily | string | — | 서체. 기본은 페이지의 monospace |
| fontSize | number | string | — | size 사다리가 고른 크기를 덮어씁니다 |
| lineHeight | number | string | — | 행간. 맨 숫자는 CSS처럼 비율입니다 |
| letterSpacing | number | string | — | 자간 |
| copyLabel | string | 'Copy' | 복사 버튼의 말 |
| copiedLabel | string | 'Copied' | 클립보드가 받은 뒤의 말 |
| copyFailedLabel | string | 'Could not copy' | 클립보드가 거절했을 때의 말 |
| rawLabel | string | 'Raw' | raw 토글의 말 |
| codeLabel | string | 'Code' | title도 language도 없을 때 영역의 이름 |
| onCopy | (code: string) => void | — | 클립보드가 코드를 받은 뒤 그 코드와 함께 호출됩니다 |
| size공통 | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | 타입 스케일과 코드 주변의 여백 |
| color공통 | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'primary' | 의미론적 색 역할. focus ring에만 닿습니다 — 블록 자체는 일부러 색 계열을 거부합니다 |
| density공통 | 'default' | 'compact' | 'default' | 코드 주변의 여백. 타입 스케일은 아닙니다 |
| elevation공통 | 0 | 1 | 2 | 3 | 0 | 그림자 깊이. 0은 평평합니다 |
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| code * | String | — | 코드. 블록 끝의 공백은 잘라 냅니다 |
| lines | List<PlCodeLine>? | — | 이미 색이 입혀진 같은 코드, 한 줄에 하나씩. 호출자의 하이라이터가 만든 것입니다 |
| language | String? | — | 무엇으로 쓰였는지. 바에 적히고, 그 외에는 아무도 읽지 않습니다 |
| theme | String | 'dark' | 팔레트. auto를 빼면 페이지의 명암과 무관합니다. 등록되지 않은 이름도 받습니다 |
| customTheme | PlCodeTheme? | — | 호출자가 가져온 팔레트. theme보다 우선합니다 |
| toolbar | bool | true | 코드 위의 바. 끄면 showLanguage·copyable·rawToggle이 무엇을 말하든 아무것도 그리지 않습니다 |
| title | Widget? | — | 바 앞쪽의 이름. 보통 파일 경로입니다 |
| showLanguage | bool | true | 바에 언어 이름을 적습니다 |
| copyable | bool | true | 코드를 클립보드에 올리는 버튼 |
| rawToggle | bool | false | 색을 걷어 내고 문자 그대로 보여 주는 토글 |
| highlightLines | String? | — | 표시할 줄: '4', '4-9', '1,4-9,12'. React의 number-or-list 대신 문자열 하나입니다 |
| lineNumbers | bool | false | 옆에 줄 번호를 붙입니다 |
| startLine | int | 1 | 첫 줄의 번호 |
| prompt | String? | — | 내용이 있는 모든 줄 앞의 셸 프롬프트 — $, #, >>>. 그려지지만 복사되지는 않습니다 |
| wrap | bool | false | 긴 줄을 옆으로 흘리는 대신 접습니다 |
| maxHeight | double? | — | 이 높이를 넘으면 안에서 스크롤합니다. 숫자는 픽셀 |
| fontFamily | String? | — | 서체. 기본은 페이지의 monospace |
| fontSize | double? | — | size 사다리가 고른 크기를 덮어씁니다 |
| lineHeight | double? | — | 행간. 글자 크기의 배수입니다 |
| letterSpacing | double? | — | 자간 |
| copyLabel | String? | 'Copy' | 복사 버튼의 말 |
| copiedLabel | String? | 'Copied' | 클립보드가 받은 뒤의 말 |
| copyFailedLabel | String? | 'Could not copy' | 클립보드가 거절했을 때의 말 |
| rawLabel | String? | 'Raw' | raw 토글의 말 |
| codeLabel | String? | 'Code' | title도 language도 없을 때 영역의 이름 |
| onCopy | ValueChanged<String>? | — | 클립보드가 코드를 받은 뒤 그 코드와 함께 호출됩니다 |
| size공통 | PlassSize | PlassSize.md | 타입 스케일과 코드 주변의 여백 |
| color공통 | PlassColor | PlassColor.primary | 의미론적 색 역할. focus ring에만 닿습니다 — 블록 자체는 일부러 색 계열을 거부합니다 |
| density공통 | PlassDensity | PlassDensity.standard | 코드 주변의 여백. 타입 스케일은 아닙니다 |
| elevation공통 | int | 0 | 그림자 깊이. 0은 평평합니다 |
native <div> 속성은 그대로 전달됩니다. color는 위 표의 color와 충돌해서, title과 prefix는 컴포넌트가 직접 쓰기 때문에, children은 코드가 code이기 때문에, onCopy는 이 컴포넌트의 것이 event가 아니라 텍스트와 함께 발화하기 때문에 제외됩니다.
PlCodeToken
React 패키지에는 아직 PlCodeToken가 없습니다.
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| text * | String | — | 글자들 |
| kind | PlCodeTokenKind? | — | 어느 슬롯이 색을 주는지. null이면 전경색입니다 |
PlCodeTokenKind는 테마가 선언하는 열두 슬롯입니다. comment, keyword, string, number, function, type, variable, tag, attribute, meta, addition, deletion. PlCodeTheme은 그 열둘에 배경과 전경을 더한 것이고, 블록이 쓰는 나머지 다섯 색은 그 둘에서 파생되므로 직접 만드는 팔레트는 열아홉이 아니라 열네 값입니다.
라이브러리 전체에서 공유 축이 뜻하는 바는 prop 규약에 있습니다.
Examples
theme
팔레트는 auto를 빼면 페이지의 명암과 무관합니다. 기본은 dark이고, 이것만은 취향이 아닙니다. 코드는 터미널 이래로 어두운 바탕에서 읽혀 왔고, 페이지를 따라 하얘지는 블록은 그 페이지에서 색을 코드가 아닌 다른 것이 정한 유일한 요소가 됩니다.
넷은 이 라이브러리의 것입니다. dark, light, auto, 그리고 색상이 아예 없이 구조를 굵기로만 나르는 mono. 나머지 여덟은 발표된 hex 그대로 옮겨 온 것입니다. one-dark, dracula, monokai, nord, night-owl, gruvbox, github, solarized-light. 코드 블록은 읽는 사람이 이미 색에 대한 의견을 있는 유일한 컴포넌트입니다.
theme은 아무 문자열이나 받습니다. 프로젝트가 자기 것을 가져오는 방법입니다.
[data-code-theme='ours'] {
--p-code-bg: #101820;
--p-code-fg: #e8e8e8;
--p-code-keyword: #ff6b6b;
/* …열한 개 더 */
}슬롯은 열여섯이고 그중 다섯은 나머지 둘에서 파생되므로 선언할 필요가 없습니다. 등록할 것도, import할 것도 없습니다.
여기에는 스타일시트가 없으므로, 직접 만드는 팔레트는 CSS 블록이 아니라 customTheme에 넘기는 PlCodeTheme입니다.
PlCodeBlock(
code: source,
customTheme: const PlCodeTheme(
background: Color(0xFF101820),
foreground: Color(0xFFE8E8E8),
keyword: Color(0xFFFF6B6B),
// …열한 개 더
),
);lineNumbers
옆에 줄 번호가 붙고, startLine이 지정하는 번호부터 셉니다. highlightLines는 줄을 표시합니다. 옅은 바탕과 앞쪽 모서리의 선. gutter가 세는 방식으로 세므로, 551부터 시작하는 블록은 '553-555'로 표시합니다.
표시의 색은 페이지의 색 계열이 아니라 그 테마 자신의 잉크에서 섞어 냅니다. 그래야 열두 팔레트 모두에서 읽히고, Dracula 블록 위에 아무도 고르지 않은 색 하나가 얹히는 일이 없습니다.
prompt
내용이 있는 모든 줄 앞의 셸 프롬프트입니다. 그려지지만 복사되지는 않습니다. 붙여 넣은 $는 셸이 삼키지 못하는 $이므로, 기록은 기록인 채로 남으면서도 그대로 붙습니다.
줄 번호도 마찬가지입니다. 둘 다 텍스트 노드가 아니고, 둘 다 클립보드에 닿지 않습니다.
Colouring
highlight.js이고, dynamic import로 불러옵니다. 문법은 40킬로바이트씩이고 서른다섯 개가 있으므로, 자기 chunk로 한 언어씩, 색을 요청한 블록에 대해서만 도착합니다. highlight={false}면 아무것도 받아 오지 않습니다.
블록은 첫 프레임에 평문으로 그려지고 문법이 도착하면 스스로 색을 입힙니다. language는 흔한 표기와 확장자를 알아들으므로, fenced code block에서 복사한 값이 그대로 동작합니다. ts, tsx, js, sh, yml, dart, py, rb, rs, md.
아무것도 모르는 언어는 거절하지 않고 평문으로 그립니다. registerLanguage로 가르치세요.
import { registerLanguage } from 'plass-ui';
import elixir from 'highlight.js/lib/languages/elixir';
registerLanguage('elixir', elixir);module scope에서 부르세요. 이미 그려진 블록을 다시 칠하지는 않지만, 그 뒤에 mount되는 블록은 전부 봅니다.
rawToggle은 색을 걷어 내고 문자 그대로 보여 주는 두 번째 버튼을 바에 올립니다.
이쪽은 코드에 색을 입히지 않고 React 쪽은 입힙니다. 그쪽은 dynamic import로 highlight.js에 닿지만, 이 패키지에는 의존성이 없고, 서른다섯 개 언어의 문법을 손으로 쓰는 것은 지킬 수 없는 약속입니다.
그래서 하이라이터가 있는 호출자는 결과를 lines로 넘기고, 없는 호출자는 프레임과 열두 팔레트와 한 가지 잉크로 그려진 코드를 받습니다.
PlCodeBlock(
code: source,
language: 'dart',
lines: const <PlCodeLine>[
<PlCodeToken>[
PlCodeToken('const', PlCodeTokenKind.keyword),
PlCodeToken(' answer = '),
PlCodeToken('42', PlCodeTokenKind.number),
PlCodeToken(';'),
],
],
);rawToggle은 그 runs를 다시 한 가지 잉크로 되돌리는 버튼을 바에 올립니다. lines가 없으면 되돌릴 것이 없으므로 버튼도 그리지 않습니다.
wrap과 maxHeight
wrap은 긴 줄을 옆으로 흘리는 대신 접고, maxHeight는 블록의 높이를 묶고 코드를 그 안에서 스크롤합니다. 같은 문제에 대한 서로 다른 답이고 둘 다 켤 수 있습니다.
<PlCodeBlock code={source} language="ts" wrap maxHeight={280} />옆으로 스크롤해도 gutter와 프롬프트는 제자리에 있습니다. 행은 창의 너비가 아니라 가장 긴 줄의 너비를 가지므로, 모든 줄의 번호가 같은 자리에서 시작합니다.
Accessibility
- 코드는 이름이 붙은 focus 가능한 영역입니다.
title, 없으면 language, 그것도 없으면 코드를 뜻하는 낱말. 스크롤되는 영역은 끌 포인터가 없는 키보드로도 닿을 수 있어야 하고, focus 가능한 영역에는 이름이 있어야 합니다. - 블록 안에서 Mod + A는 그 블록을 선택합니다. 주변 페이지가 아닙니다. 코드 목록으로 tab해 들어온 사람이 원한 것은 브라우저의 기본 답이 아닙니다.
- 번호와 프롬프트가 선택에서 빠지는 이유는 클립보드에서 빠지는 이유와 같습니다. 거기에는 선택할 것이 없습니다.
- 복사 버튼은 자기 라벨을 바꾸는데, 버튼이 아니라 페이지를 읽고 있는 스크린 리더는 그것을 듣지 못합니다. 그래서 블록은
aria-live영역으로도 알립니다. 언제나 한 낱말입니다. - raw 토글은
aria-pressed를 답니다.
- 바의 각 버튼은
button: true와 자기 이름이 붙은Semantics노드이고, 안에 있는 것을 제외합니다. 복사 버튼은 자기 낱말을 지니는 동시에 그리기도 하는데, "복사, 복사"라고 들은 사람은 한 번 더 들은 것입니다.