1
0
mirror of https://github.com/lancedikson/bowser synced 2026-02-09 17:40:09 +00:00

Add DuckDuckGo browser detection

This commit is contained in:
Jonathan Kingston 2026-01-28 16:29:03 +00:00
parent 0f51d8bce8
commit b4364c7034
6 changed files with 217 additions and 17 deletions

48
index.d.ts vendored
View File

@ -7,20 +7,36 @@ export as namespace Bowser;
declare namespace Bowser { declare namespace Bowser {
/** /**
* Creates a Parser instance * User-Agent Client Hints data structure
* @param {string} UA - User agent string * @see https://developer.mozilla.org/en-US/docs/Web/API/NavigatorUAData
* @param {boolean} skipParsing
*/ */
interface ClientHints {
brands?: Array<{ brand: string; version: string }>;
mobile?: boolean;
platform?: string;
platformVersion?: string;
architecture?: string;
model?: string;
wow64?: boolean;
}
/**
* Creates a Parser instance
* @param {string} UA - User agent string
* @param {boolean | ClientHints} skipParsingOrHints - Either skip parsing flag or Client Hints
* @param {ClientHints} clientHints - User-Agent Client Hints data
*/
function getParser(UA: string, skipParsing?: boolean): Parser.Parser; function getParser(UA: string, skipParsing?: boolean): Parser.Parser;
function getParser(UA: string, clientHints?: ClientHints): Parser.Parser;
function getParser(UA: string, skipParsing?: boolean, clientHints?: ClientHints): Parser.Parser;
/** /**
* Creates a Parser instance and runs Parser.getResult immediately * Creates a Parser instance and runs Parser.getResult immediately
* @param UA - User agent string * @param UA - User agent string
* @param clientHints - User-Agent Client Hints data
* @returns {Parser.ParsedResult} * @returns {Parser.ParsedResult}
*/ */
function parse(UA: string, clientHints?: ClientHints): Parser.ParsedResult;
function parse(UA: string): Parser.ParsedResult;
/** /**
* Constants exposed via bowser getters * Constants exposed via bowser getters
@ -33,6 +49,28 @@ declare namespace Bowser {
namespace Parser { namespace Parser {
interface Parser { interface Parser {
constructor(UA: string, skipParsing?: boolean): Parser.Parser; constructor(UA: string, skipParsing?: boolean): Parser.Parser;
constructor(UA: string, clientHints?: ClientHints): Parser.Parser;
constructor(UA: string, skipParsing?: boolean, clientHints?: ClientHints): Parser.Parser;
/**
* Get Client Hints data
* @return {ClientHints|null}
*/
getHints(): ClientHints | null;
/**
* Check if a brand exists in Client Hints brands array
* @param {string} brandName The brand name to check for
* @return {boolean}
*/
hasBrand(brandName: string): boolean;
/**
* Get brand version from Client Hints
* @param {string} brandName The brand name to get version for
* @return {string|undefined}
*/
getBrandVersion(brandName: string): string | undefined;
/** /**
* Check if the version is equals the browser version * Check if the version is equals the browser version

View File

@ -28,33 +28,49 @@ class Bowser {
* Creates a {@link Parser} instance * Creates a {@link Parser} instance
* *
* @param {String} UA UserAgent string * @param {String} UA UserAgent string
* @param {Boolean} [skipParsing=false] Will make the Parser postpone parsing until you ask it * @param {Boolean|Object} [skipParsingOrHints=false] Either a boolean to skip parsing,
* explicitly. Same as `skipParsing` for {@link Parser}. * or a ClientHints object (navigator.userAgentData)
* @param {Object} [clientHints] User-Agent Client Hints data (navigator.userAgentData)
* @returns {Parser} * @returns {Parser}
* @throws {Error} when UA is not a String * @throws {Error} when UA is not a String
* *
* @example * @example
* const parser = Bowser.getParser(window.navigator.userAgent); * const parser = Bowser.getParser(window.navigator.userAgent);
* const result = parser.getResult(); * const result = parser.getResult();
*
* @example
* // With User-Agent Client Hints
* const parser = Bowser.getParser(
* window.navigator.userAgent,
* window.navigator.userAgentData
* );
*/ */
static getParser(UA, skipParsing = false) { static getParser(UA, skipParsingOrHints = false, clientHints = null) {
if (typeof UA !== 'string') { if (typeof UA !== 'string') {
throw new Error('UserAgent should be a string'); throw new Error('UserAgent should be a string');
} }
return new Parser(UA, skipParsing); return new Parser(UA, skipParsingOrHints, clientHints);
} }
/** /**
* Creates a {@link Parser} instance and runs {@link Parser.getResult} immediately * Creates a {@link Parser} instance and runs {@link Parser.getResult} immediately
* *
* @param UA * @param {String} UA UserAgent string
* @param {Object} [clientHints] User-Agent Client Hints data (navigator.userAgentData)
* @return {ParsedResult} * @return {ParsedResult}
* *
* @example * @example
* const result = Bowser.parse(window.navigator.userAgent); * const result = Bowser.parse(window.navigator.userAgent);
*
* @example
* // With User-Agent Client Hints
* const result = Bowser.parse(
* window.navigator.userAgent,
* window.navigator.userAgentData
* );
*/ */
static parse(UA) { static parse(UA, clientHints = null) {
return (new Parser(UA)).getResult(); return (new Parser(UA, clientHints)).getResult();
} }
static get BROWSER_MAP() { static get BROWSER_MAP() {

View File

@ -14,6 +14,7 @@ export const BROWSER_ALIASES_MAP = {
Chromium: 'chromium', Chromium: 'chromium',
Diffbot: 'diffbot', Diffbot: 'diffbot',
DuckDuckBot: 'duckduckbot', DuckDuckBot: 'duckduckbot',
DuckDuckGo: 'duckduckgo',
Electron: 'electron', Electron: 'electron',
Epiphany: 'epiphany', Epiphany: 'epiphany',
FacebookExternalHit: 'facebookexternalhit', FacebookExternalHit: 'facebookexternalhit',
@ -81,6 +82,7 @@ export const BROWSER_MAP = {
chromium: 'Chromium', chromium: 'Chromium',
diffbot: 'Diffbot', diffbot: 'Diffbot',
duckduckbot: 'DuckDuckBot', duckduckbot: 'DuckDuckBot',
duckduckgo: 'DuckDuckGo',
edge: 'Microsoft Edge', edge: 'Microsoft Edge',
electron: 'Electron', electron: 'Electron',
epiphany: 'Epiphany', epiphany: 'Epiphany',

View File

@ -970,6 +970,38 @@ const browsersList = [
return browser; return browser;
}, },
}, },
/* DuckDuckGo Browser */
{
test(parser) {
// WebKit platforms (iOS, macOS): check UA string for Ddg/version suffix
const isWebKitDDG = parser.test(/\sDdg\/[\d.]+$/i);
// Chromium platforms (Android, Windows): check Client Hints brands
const isChromiumDDG = parser.hasBrand('DuckDuckGo');
return isWebKitDDG || isChromiumDDG;
},
describe(ua, parser) {
const browser = {
name: 'DuckDuckGo',
};
// Try WebKit UA pattern first
const uaVersion = Utils.getFirstMatch(/\sDdg\/([\d.]+)$/i, ua);
if (uaVersion) {
browser.version = uaVersion;
return browser;
}
// Try Client Hints brand version
if (parser) {
const hintsVersion = parser.getBrandVersion('DuckDuckGo');
if (hintsVersion) {
browser.version = hintsVersion;
}
}
return browser;
},
},
{ {
test: [/chromium/i], test: [/chromium/i],
describe(ua) { describe(ua) {

View File

@ -4,6 +4,17 @@ import platformParsersList from './parser-platforms.js';
import enginesParsersList from './parser-engines.js'; import enginesParsersList from './parser-engines.js';
import Utils from './utils.js'; import Utils from './utils.js';
/**
* @typedef {Object} ClientHints
* @property {Array<{brand: string, version: string}>} [brands] Array of brand objects
* @property {boolean} [mobile] Whether the device is mobile
* @property {string} [platform] Platform name (e.g., "Windows", "macOS")
* @property {string} [platformVersion] Platform version
* @property {string} [architecture] CPU architecture
* @property {string} [model] Device model
* @property {boolean} [wow64] Whether running under WoW64
*/
/** /**
* The main class that arranges the whole parsing process. * The main class that arranges the whole parsing process.
*/ */
@ -12,21 +23,32 @@ class Parser {
* Create instance of Parser * Create instance of Parser
* *
* @param {String} UA User-Agent string * @param {String} UA User-Agent string
* @param {Boolean} [skipParsing=false] parser can skip parsing in purpose of performance * @param {Boolean|ClientHints} [skipParsingOrHints=false] Either a boolean to skip parsing,
* improvements if you need to make a more particular parsing * or a ClientHints object containing User-Agent Client Hints data
* like {@link Parser#parseBrowser} or {@link Parser#parsePlatform} * @param {ClientHints} [clientHints] User-Agent Client Hints data (navigator.userAgentData)
* *
* @throw {Error} in case of empty UA String * @throw {Error} in case of empty UA String
* *
* @constructor * @constructor
*/ */
constructor(UA, skipParsing = false) { constructor(UA, skipParsingOrHints = false, clientHints = null) {
if (UA === void (0) || UA === null || UA === '') { if (UA === void (0) || UA === null || UA === '') {
throw new Error("UserAgent parameter can't be empty"); throw new Error("UserAgent parameter can't be empty");
} }
this._ua = UA; this._ua = UA;
// Handle overloaded constructor: (UA, clientHints) or (UA, skipParsing, clientHints)
let skipParsing = false;
if (typeof skipParsingOrHints === 'boolean') {
skipParsing = skipParsingOrHints;
this._hints = clientHints;
} else if (typeof skipParsingOrHints === 'object' && skipParsingOrHints !== null) {
this._hints = skipParsingOrHints;
} else {
this._hints = null;
}
/** /**
* @typedef ParsedResult * @typedef ParsedResult
* @property {Object} browser * @property {Object} browser
@ -56,6 +78,51 @@ class Parser {
} }
} }
/**
* Get Client Hints data
* @return {ClientHints|null}
*
* @public
*/
getHints() {
return this._hints;
}
/**
* Check if a brand exists in Client Hints brands array
* @param {string} brandName The brand name to check for
* @return {boolean}
*
* @public
*/
hasBrand(brandName) {
if (!this._hints || !Array.isArray(this._hints.brands)) {
return false;
}
const brandLower = brandName.toLowerCase();
return this._hints.brands.some(
b => b.brand && b.brand.toLowerCase() === brandLower,
);
}
/**
* Get brand version from Client Hints
* @param {string} brandName The brand name to get version for
* @return {string|undefined}
*
* @public
*/
getBrandVersion(brandName) {
if (!this._hints || !Array.isArray(this._hints.brands)) {
return undefined;
}
const brandLower = brandName.toLowerCase();
const brand = this._hints.brands.find(
b => b.brand && b.brand.toLowerCase() === brandLower,
);
return brand ? brand.version : undefined;
}
/** /**
* Get UserAgent string of current Parser instance * Get UserAgent string of current Parser instance
* @return {String} User-Agent String of the current <Parser> object * @return {String} User-Agent String of the current <Parser> object
@ -95,7 +162,7 @@ class Parser {
}); });
if (browserDescriptor) { if (browserDescriptor) {
this.parsedResult.browser = browserDescriptor.describe(this.getUA()); this.parsedResult.browser = browserDescriptor.describe(this.getUA(), this);
} }
return this.parsedResult.browser; return this.parsedResult.browser;

View File

@ -2927,6 +2927,51 @@
type: "bot" type: "bot"
vendor: "DuckDuckGo" vendor: "DuckDuckGo"
engine: {} engine: {}
DuckDuckGo:
-
ua: "Mozilla/5.0 (iPhone; CPU iPhone OS 18_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 Ddg/7.123.0"
spec:
browser:
name: "DuckDuckGo"
version: "7.123.0"
os:
name: "iOS"
version: "18.2"
platform:
type: "mobile"
vendor: "Apple"
model: "iPhone"
engine:
name: "WebKit"
-
ua: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Ddg/7.123.0"
spec:
browser:
name: "DuckDuckGo"
version: "7.123.0"
os:
name: "macOS"
version: "10.15.7"
versionName: "Catalina"
platform:
type: "desktop"
engine:
name: "WebKit"
-
ua: "Mozilla/5.0 (iPad; CPU OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 Ddg/7.120.0"
spec:
browser:
name: "DuckDuckGo"
version: "7.120.0"
os:
name: "iOS"
version: "17.5"
platform:
type: "tablet"
vendor: "Apple"
model: "iPad"
engine:
name: "WebKit"
InternetArchiveCrawler: InternetArchiveCrawler:
- -
ua: "ia_archiver" ua: "ia_archiver"