Skip to content

Add Snippets support to Github Files #832

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
14 changes: 12 additions & 2 deletions integrations/github-files/gitbook-manifest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,17 @@ externalLinks:
summary: |
# Overview

The GitHub Files integration allows you to take a link to a GitHub file or a permalink to lines of code and display them into code blocks in GitBook.
The GitHub Files integration allows you to take a link to a GitHub file or a permalink to lines of code and display them into code blocks in GitBook. It also supports snippet tags for extracting specific code sections.

# How it works

After installing the GitHub Files integration, you're able to insert it into a GitBook file in the (CMD + /) menu.

Insert the integration, paste your link, and the integration will display the code in a formatted code block.
**GitHub Files**: Insert the integration, paste your link, and the integration will display the code in a formatted code block.

**GitHub Snippet**: Insert the snippet block, provide a GitHub URL and a snippet tag (e.g., "BaseOAuthExample"), and the integration will extract and display only the code between the `--8<-- [start:tag]` and `--8<-- [end:tag]` markers. You can also add multiple snippets to combine them into a single code block.

**Simple GitHub Reference**: Insert the simple GitHub block and provide repository details (owner, repo, branch, file path) for an easier way to reference files without full URLs.
Comment on lines +23 to +25
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these could probably use some work on clarity


# Configure

Expand All @@ -31,6 +35,12 @@ blocks:
description: Insert a GitHub file as a code block
urlUnfurl:
- https://github.com/**
- id: github-snippet-block
title: GitHub Snippet
description: Insert a GitHub file snippet using tags (supports multiple snippets)
- id: github-simple-block
title: Simple GitHub Reference
description: Reference GitHub files using owner/repo/branch/file format
configurations:
account:
properties:
Expand Down
148 changes: 111 additions & 37 deletions integrations/github-files/src/github.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import { ExposableError } from '@gitbook/runtime';
import { GithubInstallationConfiguration, GithubRuntimeContext } from './types';
import { GithubInstallationConfiguration, GithubRuntimeContext, GithubSnippetProps } from './types';

export interface GithubProps {
url: string;
}

const constructGithubUrl = (owner: string, repo: string, branch: string, filePath: string) => {
return `https://github.com/${owner}/${repo}/blob/${branch}/${filePath}`;
};

Comment on lines +8 to +11
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

technically this won't work for self hosted github enterprise server instances, not sure if that matters. If so, we can change the config to include a baseline root URL

const splitGithubUrl = (url: string) => {
const permalinkRegex =
/^https?:\/\/github\.com\/([\w-]+)\/([\w-]+)\/blob\/([a-f0-9]+)\/(.+?)#(.+)$/;
const wholeFileRegex = /^https?:\/\/github\.com\/([\w-]+)\/([\w-]+)\/blob\/([\w.-]+)\/(.+)$/;
// Enhanced patterns to handle more GitHub URL formats including branches, tags, and commits
const generalBlobRegex = /^https?:\/\/github\.com\/([\w-]+)\/([\w-]+)\/blob\/([^\/]+)\/(.+?)(?:#(.+))?$/;
const multipleLineRegex = /^L\d+-L\d+$/;

let orgName = '';
Expand All @@ -17,41 +20,27 @@ const splitGithubUrl = (url: string) => {
let fileName = '';
let lines: number[] = [];

if (url.match(permalinkRegex)) {
const match = url.match(permalinkRegex);
if (!match) {
return;
}

orgName = match[1];
repoName = match[2];
ref = match[3];
fileName = match[4];
const hash = match[5];

if (hash !== '') {
if (url.match(permalinkRegex)) {
if (hash.match(multipleLineRegex)) {
lines = hash.replace(/L/g, '').split('-').map(Number);
} else {
const singleLineNumberArray: number[] = [];
const parsedInt = parseInt(hash.replace(/L/g, ''), 10);
singleLineNumberArray.push(parsedInt);
singleLineNumberArray.push(parsedInt);
lines = singleLineNumberArray;
}
// Try to match general blob pattern (handles branches, tags, commits)
const generalMatch = url.match(generalBlobRegex);
if (generalMatch) {
orgName = generalMatch[1];
repoName = generalMatch[2];
ref = generalMatch[3];
fileName = generalMatch[4];
const hash = generalMatch[5];

// Handle line numbers if present
if (hash && hash !== '') {
if (hash.match(multipleLineRegex)) {
lines = hash.replace(/L/g, '').split('-').map(Number);
} else if (hash.startsWith('L')) {
const singleLineNumberArray: number[] = [];
const parsedInt = parseInt(hash.replace(/L/g, ''), 10);
singleLineNumberArray.push(parsedInt);
singleLineNumberArray.push(parsedInt);
lines = singleLineNumberArray;
}
}
} else if (url.match(wholeFileRegex)) {
const match = url.match(wholeFileRegex);
if (!match) {
return;
}

orgName = match[1];
repoName = match[2];
ref = match[3];
fileName = match[4];
}
return {
orgName,
Expand All @@ -66,6 +55,31 @@ const getLinesFromGithubFile = (content: string[], lines: number[]) => {
return content.slice(lines[0] - 1, lines[1]);
};

const extractSnippetSection = (content: string, snippetTag: string) => {
const lines = content.split('\n');
const startMarker = `--8<-- [start:${snippetTag}]`;
const endMarker = `--8<-- [end:${snippetTag}]`;

let startIndex = -1;
let endIndex = -1;

for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (line.includes(startMarker)) {
startIndex = i + 1; // Start from the line after the marker
} else if (line.includes(endMarker) && startIndex !== -1) {
endIndex = i; // End at the line before the marker
break;
}
}

if (startIndex === -1 || endIndex === -1) {
return null; // Snippet tag not found
}

return lines.slice(startIndex, endIndex).join('\n');
};

const getHeaders = (authorise: boolean, accessToken = '') => {
const headers: { 'User-Agent': string; Authorization?: string } = {
'User-Agent': 'request',
Expand Down Expand Up @@ -144,3 +158,63 @@ export const getGithubContent = async (url: string, context: GithubRuntimeContex

return { content, fileName: urlObject.fileName };
};

export const getGithubSnippetContent = async (
url: string,
snippetTag: string,
context: GithubRuntimeContext,
) => {
const urlObject = splitGithubUrl(url);
if (!urlObject) {
return;
}

let content: string | boolean = '';
const configuration = context.environment.installation
?.configuration as GithubInstallationConfiguration;
const accessToken = configuration.oauth_credentials?.access_token;
if (!accessToken) {
throw new ExposableError('Integration is not authenticated with GitHub');
}

content = await fetchGithubFile(
urlObject.orgName,
urlObject.repoName,
urlObject.fileName,
urlObject.ref,
accessToken,
);

if (content && snippetTag) {
const snippetContent = extractSnippetSection(content, snippetTag);
if (snippetContent === null) {
throw new ExposableError(`Snippet tag '${snippetTag}' not found in file`);
}
content = snippetContent;
}

return { content, fileName: urlObject.fileName };
};

export const getGithubContentByParams = async (
owner: string,
repo: string,
branch: string,
filePath: string,
context: GithubRuntimeContext,
) => {
const url = constructGithubUrl(owner, repo, branch, filePath);
return await getGithubContent(url, context);
};

export const getGithubSnippetContentByParams = async (
owner: string,
repo: string,
branch: string,
filePath: string,
snippetTag: string,
context: GithubRuntimeContext,
) => {
const url = constructGithubUrl(owner, repo, branch, filePath);
return await getGithubSnippetContent(url, snippetTag, context);
};
Loading