Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,13 @@ like `line-numbers` depend on. Without `prism-plugin`, code is rendered via `Pri
`hljs.highlight` into `v-html`. Users import plugin JS/CSS themselves and add the plugin class
(e.g. `class="line-numbers"`), which is passed through to the `<pre>`.

## Attributes

The component sets `inheritAttrs: false` and places attributes itself: `class` goes on both the
wrapper and the `<pre>` (wrapper styling plus Prism plugin classes), `style` on the wrapper only, and
everything else (`id`, `data-*`, `aria-*`, listeners) on the `<pre>` only. Keep `class` duplicated;
Prism plugins need it on the `<pre>`.

## Build Process

```bash
Expand All @@ -88,6 +95,18 @@ Tests live in `src/plugin/__tests__/`, `src/plugin/components/__tests__/`, and
`src/plugin/composables/__tests__/`. Update snapshots with `pnpm vitest --run -u` only when the
change is intended.

### Test in the playground

Unit tests run in jsdom, which does not cover real rendering, theme CSS, or Prism plugins well.
After any change to the component's rendering, attributes, styles, themes, or plugins, also check
it in the browser playground at http://127.0.0.1:3000/vue-code-block/playground/ (served by
`pnpm dev` / `pnpm play`).

- The playground page is `src/playground/PlaygroundPage.ts.vue`. It is gitignored scratch space
(created from `src/playground/configs/templates/` if missing), so edit it freely to set up the
case being tested, and restore it afterwards.
- Check the rendered DOM and the browser console, not just that the page loads.

## Linting

```bash
Expand Down
27 changes: 25 additions & 2 deletions src/plugin/VCodeBlock.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
<template>
<div :class="codeBlockClasses">
<div
v-bind="wrapperAttrs()"
:class="codeBlockClasses"
>
<div
v-if="settings.label || settings.tabs || slots.label || slots.tabs"
class="v-code-block--header"
Expand Down Expand Up @@ -92,7 +95,7 @@
</div>

<pre
v-bind="$attrs"
v-bind="preAttrs()"
:class="`language-${settings.lang}`"
:style="preTagStyles"
>
Expand Down Expand Up @@ -146,8 +149,12 @@ const prismThemesVersion = '1.9.0';


// -------------------------------------------------- Emits & Slots & Injects //
// Attributes are placed manually so they are not duplicated on the wrapper and the <pre> //
defineOptions({ inheritAttrs: false });

const emit = defineEmits(['run', 'update:copy-status']);
const slots = useSlots();
const attrs = useAttrs();


// -------------------------------------------------- Props //
Expand Down Expand Up @@ -189,6 +196,22 @@ const runTextValue = ref<Settings['runText']>('');
const useTheme = ref<Settings['theme']>('');


// -------------------------------------------------- Attributes //
// class goes on both elements (wrapper styling + Prism plugin classes like line-numbers), //
// style goes on the wrapper, everything else (id, data-*, aria-*, listeners) goes on the <pre> //
// These are functions, not computed, because attrs is not reactive //
function wrapperAttrs(): Record<string, unknown> {
return {
class: attrs.class,
style: attrs.style,
};
}

function preAttrs(): Record<string, unknown> {
return Object.fromEntries(Object.entries(attrs).filter(([key]) => key !== 'style'));
}


// -------------------------------------------------- Computed //
// Prism plugins (line-numbers, etc.) only run when Prism highlights the element itself //
const usePrismPlugin = computed<boolean>(() => !!(settings.value.prismjs && settings.value.prismPlugin));
Expand Down
59 changes: 59 additions & 0 deletions src/plugin/__tests__/VCodeBlock.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,65 @@ describe('VCodeBlock Component', () => {
});


// -------------------------------------------------- Attributes //
describe('Attributes', () => {
it('should put the class on both the wrapper and the pre', () => {
const wrapper = mount(VCodeBlock, {
attrs: { class: 'foo' },
props: { highlightjs: true }
});

expect(wrapper.classes()).toContain('foo');
expect(wrapper.classes()).toContain('v-code-block');
expect(wrapper.find('pre').classes()).toContain('foo');
expect(wrapper.find('pre').classes()).toContain('language-javascript');
});

it('should put the style on the wrapper only', () => {
const wrapper = mount(VCodeBlock, {
attrs: { style: 'margin: 10px;' },
props: { highlightjs: true }
});

expect(wrapper.attributes('style')).toContain('margin: 10px');
expect(wrapper.find('pre').attributes('style')).not.toContain('margin');
});

it('should put the id, data and aria attributes on the pre only', () => {
const wrapper = mount(VCodeBlock, {
attrs: {
'aria-label': 'Example code',
'data-line': '2',
id: 'example',
},
props: { highlightjs: true }
});

const pre = wrapper.find('pre');

expect(wrapper.findAll('#example')).toHaveLength(1);
expect(pre.attributes('id')).toBe('example');
expect(pre.attributes('data-line')).toBe('2');
expect(pre.attributes('aria-label')).toBe('Example code');
expect(wrapper.attributes('id')).toBeUndefined();
expect(wrapper.attributes('data-line')).toBeUndefined();
expect(wrapper.attributes('aria-label')).toBeUndefined();
});

it('should only call a click listener once', async () => {
const onClick = vi.fn();
const wrapper = mount(VCodeBlock, {
attrs: { onClick },
props: { highlightjs: true }
});

await wrapper.find('pre').trigger('click');

expect(onClick).toHaveBeenCalledTimes(1);
});
});


// -------------------------------------------------- Prism Plugins //
describe('Prism Plugins', () => {
it('should render line numbers when using the prism line-numbers plugin', async () => {
Expand Down
Loading