Skip to content
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

Use the MDN API rather than using DOMParser #162

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
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
47 changes: 47 additions & 0 deletions src/commands/mdn/__snapshots__/api.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`updatedMDNQuery should work 1`] = `
Array [
Array [
Object {
"embed": Object {
"author": null,
"color": 8638706,
"description": "1. [**Document directive** - CSP document directives are used in a Conten...](https://developer.mozilla.org/Glossary/Document_directive)
2. [**document environment** - When the JavaScript global environment is ...](https://developer.mozilla.org/Glossary/document_environment)
3. [**DOM (Document Object Model)** - The DOM (Document Object Model) is ...](https://developer.mozilla.org/Glossary/DOM)
4. [**Archived open Web documentation** - The documentation listed below ...](https://developer.mozilla.org/Archive/Web)
5. [**Document.documentElement** - Document.documentElement returns the E...](https://developer.mozilla.org/Web/API/Document/documentElement)
6. [**Document.documentURI** - The documentURI read-only property of the ...](https://developer.mozilla.org/Web/API/Document/documentURI)
7. [**Document.documentURIObject** - The Document.documentURIObject read-...](https://developer.mozilla.org/Web/API/Document/documentURIObject)
8. [**Document** - The Document interface represents any web page loaded ...](https://developer.mozilla.org/Web/API/Document)
9. [**Document()** - The Document constructor creates a new Document obje...](https://developer.mozilla.org/Web/API/Document/Document)
10. [**@document** - The @document CSS at-rule restricts the style rules c...](https://developer.mozilla.org/Web/CSS/@document)

:bulb: *react with a number (:one:, :two:, ...) to filter your result*
:neutral_face: *react with \`❌\` to delete*
:point_up: *supports \`!mdn\`, \`!github\`, \`!caniuse\`, \`!npm\`, \`!composer\`, \`!bundlephobia\`, and \`!php\`*
:gear: *issues? feature requests? head over to [github](https://github.com/ljosberinn/webdev-support-bot)*",
"fields": Array [],
"footer": Object {
"iconURL": "https://avatars0.githubusercontent.com/u/7565578",
"text": "10 results found",
},
"title": "MDN results for *Search Term*",
"url": "https://developer.mozilla.org/en-US/search?q=Search%20Term",
},
},
],
]
`;

exports[`updatedMDNQuery should work 2`] = `
Array [
Array [
"https://developer.mozilla.org/Glossary/DOM",
Object {
"embed": null,
},
],
]
`;
145 changes: 145 additions & 0 deletions src/commands/mdn/api.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { updatedQueryBuilder } from './api';

import useData from '../../utils/useData';
import { getChosenResult } from '../../utils/discordTools';

const searchResponse = {
query: 'document',
locale: 'en-US',
page: 1,
pages: 383,
start: 1,
end: 10,
next:
'https://developer.mozilla.org/api/v1/search/en-US?highlight=false&page=2&q=document',
previous: null,
count: 3823,
filters: [
{
name: 'Topics',
slug: 'topic',
options: [
{
name: 'APIs and DOM',
slug: 'api',
count: 2609,
active: true,
urls: {
active: '/api/v1/search/en-US?highlight=false&q=document&topic=api',
inactive: '/api/v1/search/en-US?highlight=false&q=document',
},
},
],
},
],
documents: [
{
title: 'Document directive',
slug: 'Glossary/Document_directive',
locale: 'en-US',
excerpt:
'CSP document directives are used in a Content-Security-Policy header and govern the properties of a document or worker environment to which a policy applies.',
},
{
title: 'document environment',
slug: 'Glossary/document_environment',
locale: 'en-US',
excerpt:
"When the JavaScript global environment is a window or an iframe, it is called a document environment. A global environment is an environment that doesn't have an outer environment.",
},
{
title: 'DOM (Document Object Model)',
slug: 'Glossary/DOM',
locale: 'en-US',
excerpt:
'The DOM (Document Object Model) is an API that represents and interacts with any HTML or XML document. The DOM is a document model loaded in the browser and representing the document as a node tree, where each node represents part of the document (e.g. an element, text string, or comment).',
},
{
title: 'Archived open Web documentation',
slug: 'Archive/Web',
locale: 'en-US',
excerpt:
'The documentation listed below is archived, obsolete material about open Web topics.',
},
{
title: 'Document.documentElement',
slug: 'Web/API/Document/documentElement',
locale: 'en-US',
excerpt:
'Document.documentElement returns the Element that is the root element of the document (for example, the html element for HTML documents).',
},
{
title: 'Document.documentURI',
slug: 'Web/API/Document/documentURI',
locale: 'en-US',
excerpt:
'The documentURI read-only property of the Document interface returns the document location as a string.',
},
{
title: 'Document.documentURIObject',
slug: 'Web/API/Document/documentURIObject',
locale: 'en-US',
excerpt:
'The Document.documentURIObject read-only property returns an nsIURI object representing the URI of the document.',
},
{
title: 'Document',
slug: 'Web/API/Document',
locale: 'en-US',
excerpt:
"The Document interface represents any web page loaded in the browser and serves as an entry point into the web page's content, which is the DOM tree.",
},
{
title: 'Document()',
slug: 'Web/API/Document/Document',
locale: 'en-US',
excerpt:
"The Document constructor creates a new Document object that is a web page loaded in the browser and serving as an entry point into the page's content.",
},
{
title: '@document',
slug: 'Web/CSS/@document',
locale: 'en-US',
excerpt:
'The @document CSS at-rule restricts the style rules contained within it based on the URL of the document. It is designed primarily for user-defined style sheets, though it can be used on author-defined style sheets, too.',
},
],
};

describe('updatedMDNQuery', () => {
const mockUseData: jest.MockedFunction<typeof useData> = jest.fn();
const mockChoose: jest.MockedFunction<typeof getChosenResult> = jest.fn();

const editMsg = {
edit: jest.fn(),
};
const sendMock = jest.fn();
const replyMock = jest.fn();
const msg: any = {
channel: { send: sendMock },
reply: replyMock,
};

test('should work', async () => {
mockUseData.mockResolvedValueOnce({
error: false,
text: null,
json: searchResponse,
});

mockChoose.mockResolvedValueOnce({
title: 'DOM (Document Object Model)',
slug: 'Glossary/DOM',
locale: 'en-US',
excerpt:
'The DOM (Document Object Model) is an API that represents and interacts with any HTML or XML document. The DOM is a document model loaded in the browser and representing the document as a node tree, where each node represents part of the document (e.g. an element, text string, or comment).',
});

sendMock.mockResolvedValue(editMsg);
const handler = updatedQueryBuilder(mockUseData, mockChoose);

await handler(msg, 'Search Term');
expect(msg.channel.send.mock.calls).toMatchSnapshot();
expect(editMsg.edit.mock.calls).toMatchSnapshot();
});
});
119 changes: 119 additions & 0 deletions src/commands/mdn/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/* eslint-disable unicorn/prefer-query-selector */
import { Message } from 'discord.js';

import delayedMessageAutoDeletion from '../../utils/delayedMessageAutoDeletion';
import {
adjustTitleLength,
attemptEdit,
BASE_DESCRIPTION,
createDescription,
createListEmbed,
createMarkdownLink,
createMarkdownListItem,
getChosenResult,
} from '../../utils/discordTools';
import * as errors from '../../utils/errors';
import { buildDirectUrl, getSearchUrl } from '../../utils/urlTools';
import useData from '../../utils/useData';

const provider = 'mdn';

interface SearchResponse {
query: string;
locale: string;
page: number;
pages: number;
starts: number;
end: number;
next: string;
previous: string | null;
count: number;
filter: Array<{
name: string;
slug: string;
options: Array<{
name: string;
slug: string;
count: number;
active: boolean;
urls: {
active: string;
inactive: string;
};
}>;
}>;
documents: Array<{
title: string;
slug: string;
locale: string;
excerpt: string;
}>;
}

export const updatedQueryBuilder = (
fetch: typeof useData = useData,
waitForChosenResult: typeof getChosenResult = getChosenResult
) => async (msg: Message, searchTerm: string) => {
try {
const url = getSearchUrl(provider, searchTerm);
const { error, json } = await fetch<SearchResponse>(url, 'json');
if (error) {
return msg.reply(errors.invalidResponse);
}

if (json.documents.length === 0) {
const sentMsg = await msg.reply(errors.noResults(searchTerm));
return delayedMessageAutoDeletion(sentMsg);
}

let preparedDescription = json.documents.map(
({ title, excerpt, slug }, index) =>
createMarkdownListItem(
index,
createMarkdownLink(
adjustTitleLength([`**${title}**`, excerpt].join(' - ')),
buildDirectUrl(provider, slug)
)
)
);

const expectedLength = preparedDescription.reduce(
(sum, item) => sum + item.length,
0
);
if (expectedLength + BASE_DESCRIPTION.length + 10 * '\n'.length > 2048) {
preparedDescription = preparedDescription.map(string => {
// split at markdown link ending
const [title, ...rest] = string.split('...]');

// split title on title - excerpt glue
// concat with rest
// fix broken markdown link ending
return [title.split(' - ')[0], rest.join('')].join(']');
});
}

const sentMsg = await msg.channel.send(
createListEmbed({
description: createDescription(preparedDescription),
footerText: `${json.documents.length} results found`,
provider,
searchTerm,
url,
})
);

const result = await waitForChosenResult(sentMsg, msg, json.documents);
if (!result) {
return;
}

const editableUrl = buildDirectUrl(provider, result.slug);
await attemptEdit(sentMsg, editableUrl, { embed: null });
} catch (error) {
console.error(error);
await msg.reply(errors.unknownError);
}
};

export default updatedQueryBuilder();
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@ import * as errors from '../../utils/errors';
import { getSearchUrl } from '../../utils/urlTools';
import useData from '../../utils/useData';

import { queryBuilder } from '.';
import { queryBuilder } from './dom';
import { getChosenResult } from '../../utils/discordTools';

jest.mock('dom-parser');
jest.mock('../../utils/urlTools');
jest.mock('../../utils/useData');

const mockGetSearchUrl: jest.MockedFunction<typeof getSearchUrl> = getSearchUrl as any;
const mockUseData: jest.MockedFunction<typeof useData> = useData as any;
const mockChoose: jest.MockedFunction<typeof getChosenResult> = getChosenResult as any;

describe('handleMDNQuery', () => {
const sendMock = jest.fn();
Expand Down
32 changes: 32 additions & 0 deletions src/commands/mdn/index.ts → src/commands/mdn/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,38 @@ interface ParserResult {
meta: string;
}

interface SearchResponse {
query: string;
locale: string;
page: number;
pages: number;
starts: number;
end: number;
next: string;
previous: string | null;
count: number;
filter: Array<{
name: string;
slug: string;
options: Array<{
name: string;
slug: string;
count: number;
active: boolean;
urls: {
active: string;
inactive: string;
};
}>;
}>;
documents: Array<{
title: string;
slug: string;
locale: string;
excerpt: string;
}>;
}

interface ResultMeta {
getElementsByClassName(cls: string): DOMParser.Node[];
}
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import handleFormattingRequest from './commands/formatting';
import handleGithubQuery from './commands/github';
import handleJQueryCommand from './commands/jquery';
import handleLeaderboardRequest from './commands/leaderboard';
import handleMDNQuery from './commands/mdn';
import handleMDNQuery from './commands/mdn/api';
import handleNPMQuery from './commands/npm';
import handlePHPQuery from './commands/php';
import handlePointsRequest from './commands/points';
Expand Down
Loading