Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/cli-search-improvements.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@embedpdf/cli": patch
---

Improve search-docs output: show descriptions in human-readable mode, reduce default limit from 10 to 5, and add minimum score threshold (0.25) to filter irrelevant results.
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,8 @@ config/tsup.frameworks.config.bundled*
publish-summary.json
**/.svelte-kit
**/*/*.tgz
.env

# Generated data files
packages/cli/src/data/manifest.json
website/src/data/embeddings.json
105 changes: 105 additions & 0 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<div align="center">
<a href="https://www.embedpdf.com">
<img alt="EmbedPDF logo" src="https://www.embedpdf.com/logo-192.png" height="96">
</a>
<h1>EmbedPDF</h1>

<a href="https://www.npmjs.com/package/@embedpdf/cli"><img alt="NPM version" src="https://img.shields.io/npm/v/@embedpdf/cli.svg?style=for-the-badge&labelColor=000000"></a> <a href="https://github.com/embedpdf/embed-pdf-viewer/blob/main/packages/cli/LICENSE"><img alt="License" src="https://img.shields.io/npm/l/@embedpdf/cli.svg?style=for-the-badge&labelColor=000000"></a> <a href="https://github.com/embedpdf/embed-pdf-viewer/discussions"><img alt="Join the community on GitHub" src="https://img.shields.io/badge/Join%20the%20community-blueviolet.svg?style=for-the-badge&labelColor=000000"></a>

</div>

# @embedpdf/cli

CLI for searching and reading EmbedPDF documentation directly from your terminal.

## Documentation

For complete guides, examples, and full API reference, visit:

**[Official Documentation](https://www.embedpdf.com/docs)**

## Installation

```bash
# npm
npm install -g @embedpdf/cli

# pnpm
pnpm add -g @embedpdf/cli

# or run directly
npx @embedpdf/cli
```

## Commands

### `embedpdf search-docs <query>`

Full-text search over the entire documentation corpus.

```bash
# Basic search
embedpdf search-docs "zoom"

# Filter by framework
embedpdf search-docs "getting started" --framework react

# Filter by section
embedpdf search-docs "annotations" --section headless

# JSON output (for LLM consumption)
embedpdf search-docs "rendering" --json --limit 5
```

| Option | Description |
|---|---|
| `--json` | Output as JSON |
| `--framework <framework>` | Filter by framework (`react`, `vue`, `svelte`) |
| `--section <section>` | Filter by section (`headless`, `viewer`, `snippet`, `engines`, `pdfium`) |
| `--limit <n>` | Maximum number of results (default: `10`) |

### `embedpdf doc [path]`

Fetch and display a documentation page.

```bash
# Fetch a specific doc
embedpdf doc react/headless/getting-started

# List all available pages
embedpdf doc --list

# List docs for a specific framework
embedpdf doc --list --framework vue

# Output as JSON
embedpdf doc react/headless/plugins/plugin-zoom --json
```

| Option | Description |
|---|---|
| `--json` | Output as JSON |
| `--list` | List all available doc pages |
| `--framework <framework>` | Filter by framework (`react`, `vue`, `svelte`) |
| `--section <section>` | Filter by section (`headless`, `viewer`, `snippet`, `engines`, `pdfium`) |

## Programmatic Usage

The package also exports functions for use in Node.js:

```typescript
import { searchDocs, fetchDocContent, getManifest } from '@embedpdf/cli';

// Search docs
const results = searchDocs('zoom', { framework: 'react', limit: 5 });

// Fetch a doc page
const content = await fetchDocContent('react/headless/getting-started');

// Get the full manifest
const manifest = getManifest();
```

## License

MIT – see the [LICENSE](https://github.com/embedpdf/embed-pdf-viewer/blob/main/packages/cli/LICENSE) file.
49 changes: 49 additions & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"name": "@embedpdf/cli",
"version": "2.14.3",
"private": false,
"description": "CLI for searching and reading EmbedPDF documentation.",
"type": "module",
"bin": {
"embedpdf": "./dist/bin.js"
},
"main": "./dist/bin.cjs",
"module": "./dist/bin.js",
"exports": {
".": {
"types": "./dist/bin.d.ts",
"import": "./dist/bin.js",
"require": "./dist/bin.cjs"
}
},
"scripts": {
"generate-manifest": "node scripts/generate-manifest.mjs",
"build": "pnpm run generate-manifest && pnpm run clean && vite build",
"clean": "rimraf dist"
},
"files": [
"dist",
"README.md"
],
"repository": {
"type": "git",
"url": "https://github.com/embedpdf/embed-pdf-viewer",
"directory": "packages/cli"
},
"homepage": "https://www.embedpdf.com/docs",
"bugs": {
"url": "https://github.com/embedpdf/embed-pdf-viewer/issues"
},
"author": "EmbedPDF",
"license": "MIT",
"dependencies": {
"commander": "^13.1.0"
},
"devDependencies": {
"@embedpdf/build": "workspace:*",
"typescript": "^5.0.0"
},
"publishConfig": {
"access": "public"
}
}
189 changes: 189 additions & 0 deletions packages/cli/scripts/generate-manifest.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
/**
* Scans website/src/content/docs/ for all MDX files,
* extracts frontmatter (title, description, searchable) and full text content,
* and writes src/lib/manifest.json.
*
* Run: node scripts/generate-manifest.mjs
*/

import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const CONTENT_DIR = path.resolve(__dirname, '../../../website/src/content/docs');
const TOOLS_DIR = path.resolve(__dirname, '../../../website/src/content/tools');
const OUTPUT_PATH = path.resolve(__dirname, '../src/data/manifest.json');

function parseFrontmatter(content) {
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
if (!match) return { fm: {}, body: content };

const fm = {};
for (const line of match[1].split('\n')) {
const colonIdx = line.indexOf(':');
if (colonIdx === -1) continue;
const key = line.slice(0, colonIdx).trim();
let value = line.slice(colonIdx + 1).trim();

// Strip surrounding quotes
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
value = value.slice(1, -1);
}

// Parse booleans
if (value === 'true') value = true;
else if (value === 'false') value = false;

fm[key] = value;
}

// Everything after the frontmatter block
const body = content.slice(match[0].length);
return { fm, body };
}

/**
* Strip MDX/JSX syntax from doc content, returning plain searchable text.
*/
function stripMdx(content) {
let text = content;

// Remove import statements
text = text.replace(/^import\s+.*?;\s*$/gm, '');

// Remove JSX component wrappers but keep text children
// e.g. <ExampleWrapper>...</ExampleWrapper>
text = text.replace(/<ExampleWrapper[^>]*>[\s\S]*?<\/ExampleWrapper>/g, '');

// Remove Callout tags but keep content
text = text.replace(/<\/?Callout[^>]*>/g, '');

// Remove self-closing JSX tags
text = text.replace(/<[A-Z][a-zA-Z]*\s*[^>]*\/>/g, '');

// Remove remaining JSX open/close tags (keep text content)
text = text.replace(/<\/?[A-Z][a-zA-Z]*[^>]*>/g, '');

// Remove fenced code blocks (they're code examples, not prose)
text = text.replace(/```[\s\S]*?```/g, '');

// Remove inline code backticks but keep the text
text = text.replace(/`([^`]+)`/g, '$1');

// Remove markdown link syntax, keep text
text = text.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1');

// Remove markdown image syntax
text = text.replace(/!\[([^\]]*)\]\([^)]+\)/g, '');

// Remove HTML comments
text = text.replace(/<!--[\s\S]*?-->/g, '');

// Remove markdown heading markers but keep text
text = text.replace(/^#{1,6}\s+/gm, '');

// Remove markdown bold/italic markers
text = text.replace(/\*{1,3}([^*]+)\*{1,3}/g, '$1');
text = text.replace(/_{1,3}([^_]+)_{1,3}/g, '$1');

// Remove horizontal rules
text = text.replace(/^[-*_]{3,}\s*$/gm, '');

// Remove markdown list markers
text = text.replace(/^\s*[-*+]\s+/gm, '');
text = text.replace(/^\s*\d+\.\s+/gm, '');

// Collapse multiple blank lines
text = text.replace(/\n{3,}/g, '\n\n');

return text.trim();
}

function inferMeta(relativePath, collection = 'docs') {
const parts = relativePath.replace(/\.mdx$/, '').split('/');

let framework = null;
let section = null;

if (collection === 'tools') {
section = 'tools';
} else if (['react', 'vue', 'svelte'].includes(parts[0])) {
framework = parts[0];
if (['headless', 'viewer'].includes(parts[1])) {
section = parts[1];
}
} else if (parts[0] === 'snippet') {
section = 'snippet';
} else if (parts[0] === 'engines') {
section = 'engines';
} else if (parts[0] === 'pdfium') {
section = 'pdfium';
}

const docPath = relativePath.replace(/\.mdx$/, '');
const urlPrefix = collection === 'tools' ? 'tools' : 'docs';
const url = `https://www.embedpdf.com/${urlPrefix}/${docPath}`;

return { docPath, url, framework, section };
}

function scanCollection(dir, collection) {
if (!fs.existsSync(dir)) return [];
const entries = fs.readdirSync(dir, { recursive: true });
const docs = [];

for (const entry of entries) {
const entryStr = String(entry);
if (!entryStr.endsWith('.mdx')) continue;

const fullPath = path.join(dir, entryStr);
const raw = fs.readFileSync(fullPath, 'utf-8');
const { fm, body } = parseFrontmatter(raw);

// Skip non-searchable pages
if (fm.searchable === false) continue;

// Skip index pages (they're usually just navigation wrappers)
const basename = path.basename(entryStr, '.mdx');
if (basename === 'index') continue;

const meta = inferMeta(entryStr, collection);
const content = stripMdx(body);

docs.push({
path: meta.docPath,
title: fm.title || basename,
description: fm.description || '',
url: meta.url,
framework: meta.framework,
section: meta.section,
content,
});
}

return docs;
}

function scanDocs() {
const docs = [
...scanCollection(CONTENT_DIR, 'docs'),
...scanCollection(TOOLS_DIR, 'tools'),
];

// Sort by path for deterministic output
docs.sort((a, b) => a.path.localeCompare(b.path));
return docs;
}

const docs = scanDocs();

fs.writeFileSync(OUTPUT_PATH, JSON.stringify({ docs }, null, 2) + '\n');

const totalContentChars = docs.reduce((sum, d) => sum + d.content.length, 0);
console.log(
`[generate-manifest] Wrote ${docs.length} docs (${(totalContentChars / 1024).toFixed(0)}KB of content) to ${path.relative(process.cwd(), OUTPUT_PATH)}`,
);
12 changes: 12 additions & 0 deletions packages/cli/src/bin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Command } from 'commander';
import { registerDocCommand } from './commands/doc';
import { registerSearchCommand } from './commands/search';

const program = new Command();

program.name('embedpdf').description('The official CLI for the EmbedPDF project').version('2.14.3');

registerDocCommand(program);
registerSearchCommand(program);

program.parse();
Loading