mirror of
https://github.com/squidfunk/mkdocs-material.git
synced 2026-08-04 19:45:08 -04:00
Restructured observables
This commit is contained in:
@@ -20,5 +20,5 @@
|
||||
* IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
export * from "./_"
|
||||
export * from "./shadow"
|
||||
export * from "./title"
|
||||
|
||||
@@ -28,7 +28,6 @@ import "../stylesheets/app-palette.scss"
|
||||
|
||||
import { values } from "ramda"
|
||||
import {
|
||||
EMPTY,
|
||||
merge,
|
||||
of,
|
||||
combineLatest,
|
||||
@@ -45,7 +44,6 @@ import {
|
||||
} from "rxjs/operators"
|
||||
|
||||
import {
|
||||
getElement,
|
||||
watchToggle,
|
||||
getElements,
|
||||
watchMedia,
|
||||
@@ -60,7 +58,7 @@ import {
|
||||
mayReceiveKeyboardEvents
|
||||
} from "./observables"
|
||||
import { setupSearchWorker } from "./workers"
|
||||
import { renderSource } from "templates"
|
||||
|
||||
import { setToggle, setScrollLock, resetScrollLock } from "actions"
|
||||
import {
|
||||
mountHeader,
|
||||
@@ -74,10 +72,10 @@ import {
|
||||
watchComponentMap,
|
||||
mountHeaderTitle
|
||||
} from "components"
|
||||
import { mountClipboard } from "./integrations/clipboard"
|
||||
import { setupClipboard } from "./integrations/clipboard"
|
||||
import { patchTables, patchDetails, patchScrollfix } from "patches"
|
||||
import { takeIf, not, isConfig } from "utilities"
|
||||
import { fetchSourceFacts } from "integrations/source"
|
||||
import { setupSourceFacts } from "integrations/source"
|
||||
import { renderDialog } from "templates/dialog"
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
@@ -89,31 +87,6 @@ document.documentElement.classList.add("js")
|
||||
if (navigator.userAgent.match(/(iPad|iPhone|iPod)/g))
|
||||
document.documentElement.classList.add("ios")
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Yes, this is a super hacky implementation. Needs clean up.
|
||||
*/
|
||||
function repository() {
|
||||
const el = getElement<HTMLAnchorElement>(".md-source[href]") // TODO: dont use classes
|
||||
// console.log(el)
|
||||
if (!el)
|
||||
return EMPTY
|
||||
|
||||
const data = sessionStorage.getItem("repository")
|
||||
if (data) {
|
||||
const x = JSON.parse(data)
|
||||
return of(x)
|
||||
}
|
||||
|
||||
return fetchSourceFacts(el.href)
|
||||
.pipe(
|
||||
tap(data => sessionStorage.setItem("repository", JSON.stringify(data)))
|
||||
)
|
||||
}
|
||||
|
||||
// memoize
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Functions
|
||||
* ------------------------------------------------------------------------- */
|
||||
@@ -214,7 +187,7 @@ export function initialize(config: unknown) {
|
||||
const dialog = renderDialog("Copied to Clipboard")
|
||||
|
||||
// snackbar for copy to clipboard
|
||||
mountClipboard({ document$ })
|
||||
setupClipboard({ document$ })
|
||||
.pipe(
|
||||
switchMap(ev => {
|
||||
ev.clearSelection()
|
||||
@@ -235,7 +208,7 @@ export function initialize(config: unknown) {
|
||||
patchTables({ document$ })
|
||||
.subscribe()
|
||||
|
||||
patchDetails({ document$ })
|
||||
patchDetails({ document$, hash$ })
|
||||
.subscribe()
|
||||
|
||||
/* Force 1px scroll offset to trigger overflow scrolling */
|
||||
@@ -244,6 +217,7 @@ export function initialize(config: unknown) {
|
||||
.subscribe()
|
||||
|
||||
|
||||
|
||||
// TODO: general keyboard handler...
|
||||
// put into main!?
|
||||
|
||||
@@ -277,28 +251,6 @@ export function initialize(config: unknown) {
|
||||
)
|
||||
.subscribe()
|
||||
|
||||
// TODO: patch details!
|
||||
|
||||
/* Open details after anchor jump */
|
||||
merge(hash$, of(location.hash)) // getLocationHash
|
||||
.subscribe(hash => {
|
||||
const el = getElement(hash)
|
||||
console.log("jump to", hash)
|
||||
if (typeof el !== "undefined") {
|
||||
const parent = el.closest("details")
|
||||
if (parent && !parent.open) { // only if it is not open!
|
||||
parent.open = true
|
||||
|
||||
/* Hack: force reload for repositioning */ // TODO. what happens here!?
|
||||
location.hash = "" // reset
|
||||
requestAnimationFrame(() => {
|
||||
location.hash = hash // tslint:disable-line
|
||||
})
|
||||
// TODO: setLocationHash() + forceLocationHashChange
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Scroll lock
|
||||
const toggle$ = useToggle("search")
|
||||
combineLatest([
|
||||
@@ -353,20 +305,9 @@ export function initialize(config: unknown) {
|
||||
|
||||
// build a notification component! feed txt into it...
|
||||
|
||||
/* ----------------------------------------------------------------------- */
|
||||
|
||||
// TODO: WIP repo rendering
|
||||
repository().subscribe(facts => {
|
||||
if (facts.length) {
|
||||
const sources = getElements(".md-source__repository")
|
||||
sources.forEach(repo => {
|
||||
repo.dataset.mdState = "done"
|
||||
repo.appendChild(
|
||||
renderSource(facts)
|
||||
)
|
||||
})
|
||||
}
|
||||
})
|
||||
setupSourceFacts({ document$ })
|
||||
.subscribe()
|
||||
|
||||
/* ----------------------------------------------------------------------- */
|
||||
|
||||
|
||||
@@ -32,9 +32,9 @@ import { renderClipboard } from "templates"
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Mount options
|
||||
* Setup options
|
||||
*/
|
||||
interface MountOptions {
|
||||
interface SetupOptions {
|
||||
document$: Observable<Document> /* Document observable */
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ interface MountOptions {
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Mount clipboard
|
||||
* Setup clipboard
|
||||
*
|
||||
* This function implements the Clipboard.js integration and injects a button
|
||||
* into all code blocks when the document changes.
|
||||
@@ -52,8 +52,8 @@ interface MountOptions {
|
||||
*
|
||||
* @return Clipboard observable
|
||||
*/
|
||||
export function mountClipboard(
|
||||
{ document$ }: MountOptions
|
||||
export function setupClipboard(
|
||||
{ document$ }: SetupOptions
|
||||
): Observable<ClipboardJS.Event> {
|
||||
if (ClipboardJS.isSupported()) {
|
||||
return document$
|
||||
@@ -69,7 +69,7 @@ export function mountClipboard(
|
||||
}
|
||||
}),
|
||||
|
||||
/* Initialize and mount clipboard */
|
||||
/* Initialize and setup clipboard */
|
||||
switchMap(() => {
|
||||
return fromEventPattern<ClipboardJS.Event>(next => {
|
||||
const clipboard = new ClipboardJS(".md-clipboard")
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2016-2020 Martin Donath <martin.donath@squidfunk.com>
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to
|
||||
* deal in the Software without restriction, including without limitation the
|
||||
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
* sell copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
* IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { Observable, of } from "rxjs"
|
||||
|
||||
import { fetchSourceFactsFromGitHub } from "../github"
|
||||
import { fetchSourceFactsFromGitLab } from "../gitlab"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Types
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Source facts
|
||||
*/
|
||||
export type SourceFacts = string[]
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Functions
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Fetch source facts
|
||||
*
|
||||
* @param url - Source repository URL
|
||||
*
|
||||
* @return Source facts observable
|
||||
*/
|
||||
export function fetchSourceFacts(
|
||||
url: string
|
||||
): Observable<SourceFacts> {
|
||||
const [type] = url.match(/(git(?:hub|lab))/i) || []
|
||||
switch (type.toLowerCase()) {
|
||||
|
||||
/* GitHub repository */
|
||||
case "github":
|
||||
const [, user, repo] = url.match(/^.+github\.com\/([^\/]+)\/?([^\/]+)/i)
|
||||
return fetchSourceFactsFromGitHub(user, repo)
|
||||
|
||||
/* GitLab repository */
|
||||
case "gitlab":
|
||||
const [, base, project] = url.match(/^.+?([^\/]*gitlab[^\/]+)\/(.+)/i)
|
||||
return fetchSourceFactsFromGitLab(base, project)
|
||||
|
||||
/* Everything else */
|
||||
default:
|
||||
return of([])
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Round a number for display with source facts
|
||||
*
|
||||
* This is a reverse engineered implementation of GitHub's weird rounding
|
||||
* algorithm for stars, forks and all other numbers. Probably incorrect.
|
||||
*
|
||||
* @param value - Original value
|
||||
*
|
||||
* @return Rounded value
|
||||
*/
|
||||
export function roundSourceFactValue(value: number) {
|
||||
if (value > 999) {
|
||||
const digits = +((value - 950) % 1000 > 99)
|
||||
return `${((value + 1) / 1000).toFixed(digits)}k`
|
||||
} else {
|
||||
return value.toString()
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,9 @@ import { Observable, of } from "rxjs"
|
||||
import { ajax } from "rxjs/ajax"
|
||||
import { filter, pluck, shareReplay, switchMap } from "rxjs/operators"
|
||||
|
||||
import { SourceFacts, roundSourceFactValue } from "../_"
|
||||
import { round } from "utilities"
|
||||
|
||||
import { SourceFacts } from ".."
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Functions
|
||||
@@ -57,15 +59,15 @@ export function fetchSourceFactsFromGitHub(
|
||||
if (typeof repo !== "undefined") {
|
||||
const { stargazers_count, forks_count }: Repo = data
|
||||
return of([
|
||||
`${roundSourceFactValue(stargazers_count || 0)} Stars`,
|
||||
`${roundSourceFactValue(forks_count || 0)} Forks`
|
||||
`${round(stargazers_count || 0)} Stars`,
|
||||
`${round(forks_count || 0)} Forks`
|
||||
])
|
||||
|
||||
/* GitHub user/organization */
|
||||
} else {
|
||||
const { public_repos }: User = data
|
||||
return of([
|
||||
`${roundSourceFactValue(public_repos || 0)} Repositories`
|
||||
`${round(public_repos || 0)} Repositories`
|
||||
])
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -25,7 +25,9 @@ import { Observable } from "rxjs"
|
||||
import { ajax } from "rxjs/ajax"
|
||||
import { filter, map, pluck, shareReplay } from "rxjs/operators"
|
||||
|
||||
import { SourceFacts, roundSourceFactValue } from "../_"
|
||||
import { round } from "utilities"
|
||||
|
||||
import { SourceFacts } from ".."
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Functions
|
||||
@@ -50,8 +52,8 @@ export function fetchSourceFactsFromGitLab(
|
||||
filter(({ status }) => status === 200),
|
||||
pluck("response"),
|
||||
map(({ star_count, forks_count }: ProjectSchema) => ([
|
||||
`${roundSourceFactValue(star_count || 0)} Stars`,
|
||||
`${roundSourceFactValue(forks_count || 0)} Forks`
|
||||
`${round(star_count)} Stars`,
|
||||
`${round(forks_count)} Forks`
|
||||
])),
|
||||
shareReplay(1)
|
||||
)
|
||||
|
||||
@@ -20,5 +20,128 @@
|
||||
* IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
export * from "./_"
|
||||
export * from "./github"
|
||||
import { NEVER, Observable, of } from "rxjs"
|
||||
import { switchMap, tap } from "rxjs/operators"
|
||||
|
||||
import { getElement, getElements } from "observables"
|
||||
import { renderSource } from "templates"
|
||||
|
||||
import { cache } from "utilities"
|
||||
import { fetchSourceFactsFromGitHub } from "./github"
|
||||
import { fetchSourceFactsFromGitLab } from "./gitlab"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Types
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Source facts
|
||||
*/
|
||||
export type SourceFacts = string[]
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Helper types
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Setup options
|
||||
*/
|
||||
interface SetupOptions {
|
||||
document$: Observable<Document> /* Document observable */
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Helper functions
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Fetch source facts
|
||||
*
|
||||
* @param url - Source repository URL
|
||||
*
|
||||
* @return Source facts observable
|
||||
*/
|
||||
function fetchSourceFacts(
|
||||
url: string
|
||||
): Observable<SourceFacts> {
|
||||
const [type] = url.match(/(git(?:hub|lab))/i) || []
|
||||
switch (type.toLowerCase()) {
|
||||
|
||||
/* GitHub repository */
|
||||
case "github":
|
||||
const [, user, repo] = url.match(/^.+github\.com\/([^\/]+)\/?([^\/]+)/i)
|
||||
return fetchSourceFactsFromGitHub(user, repo)
|
||||
|
||||
/* GitLab repository */
|
||||
case "gitlab":
|
||||
const [, base, project] = url.match(/^.+?([^\/]*gitlab[^\/]+)\/(.+)/i)
|
||||
return fetchSourceFactsFromGitLab(base, project)
|
||||
|
||||
/* Everything else */
|
||||
default:
|
||||
return of([])
|
||||
}
|
||||
}
|
||||
|
||||
// provide a function to set a unique key?
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Functions
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
// TODO: this doesnt belong here...
|
||||
|
||||
// cache type.. session or local storage...
|
||||
// subscribe to observable and cache automatically
|
||||
|
||||
// withCache or useCache
|
||||
|
||||
/**
|
||||
* Setup source facts
|
||||
*
|
||||
* @param options - Options
|
||||
*
|
||||
* @return Source facts observable
|
||||
*/
|
||||
export function setupSourceFacts(
|
||||
{ document$ }: SetupOptions
|
||||
): Observable<any> {
|
||||
return document$
|
||||
.pipe(
|
||||
switchMap(() => cache("repository", () => {
|
||||
tap(console.log)
|
||||
const el = getElement<HTMLAnchorElement>(".md-source[href]") // TODO: useElement( document$ ) ? doesnt emit if no result
|
||||
if (!el) {
|
||||
return NEVER
|
||||
} else {
|
||||
// TODO: hash should be calculated from el.href
|
||||
return fetchSourceFacts(el.href)
|
||||
}
|
||||
})),
|
||||
|
||||
tap(facts => {
|
||||
console.log(facts)
|
||||
if (facts.length) {
|
||||
const sources = getElements(".md-source__repository")
|
||||
sources.forEach(repo => {
|
||||
repo.dataset.mdState = "done"
|
||||
repo.appendChild(renderSource(facts))
|
||||
})
|
||||
}
|
||||
})
|
||||
// TODO: use URL? to dinstinguish
|
||||
)
|
||||
}
|
||||
|
||||
// /**
|
||||
// * Only emit a value if it is non-empty
|
||||
// *
|
||||
// * @template T - Value type
|
||||
// *
|
||||
// * @return Operator function
|
||||
// */
|
||||
// export function exists<T>(): OperatorFunction<T, NonNullable<T>> {
|
||||
// return pipe(
|
||||
// switchMap(value => !!value ? NEVER : of(value as NonNullable<T>))
|
||||
// )
|
||||
// }
|
||||
|
||||
@@ -21,16 +21,18 @@
|
||||
*/
|
||||
|
||||
import { identity } from "ramda"
|
||||
import { Observable, fromEvent, merge } from "rxjs"
|
||||
import { Observable, animationFrameScheduler, fromEvent, merge, of } from "rxjs"
|
||||
import {
|
||||
filter,
|
||||
map,
|
||||
observeOn,
|
||||
shareReplay,
|
||||
switchMap,
|
||||
switchMapTo,
|
||||
tap
|
||||
} from "rxjs/operators"
|
||||
|
||||
import { getElements, watchMedia } from "observables"
|
||||
import { getElement, getElements, watchMedia } from "observables"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Helper types
|
||||
@@ -41,6 +43,7 @@ import { getElements, watchMedia } from "observables"
|
||||
*/
|
||||
interface MountOptions {
|
||||
document$: Observable<Document> /* Document observable */
|
||||
hash$: Observable<string> /* Location hash observable */
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
@@ -58,19 +61,52 @@ interface MountOptions {
|
||||
* @return Details elements observable
|
||||
*/
|
||||
export function patchDetails(
|
||||
{ document$ }: MountOptions
|
||||
{ document$, hash$ }: MountOptions
|
||||
): Observable<HTMLDetailsElement[]> {
|
||||
return merge(
|
||||
const els$ = document$
|
||||
.pipe(
|
||||
map(() => getElements<HTMLDetailsElement>("details")),
|
||||
shareReplay(1)
|
||||
)
|
||||
|
||||
/* Open all details before printing */
|
||||
merge(
|
||||
watchMedia("print").pipe(filter(identity)), // Webkit
|
||||
fromEvent(window, "beforeprint") // IE, FF
|
||||
)
|
||||
.pipe(
|
||||
switchMapTo(document$),
|
||||
map(() => getElements<HTMLDetailsElement>("details")),
|
||||
tap(els => {
|
||||
for (const detail of els)
|
||||
detail.setAttribute("open", "")
|
||||
}),
|
||||
shareReplay(1)
|
||||
switchMapTo(els$)
|
||||
)
|
||||
.subscribe(els => {
|
||||
for (const el of els)
|
||||
el.setAttribute("open", "")
|
||||
})
|
||||
|
||||
/* Open details before anchor jump */
|
||||
merge(hash$, of(location.hash))
|
||||
.pipe(
|
||||
filter(hash => !!hash.length),
|
||||
switchMap(hash => of(getElement<HTMLElement>(hash) || document.body)
|
||||
.pipe(
|
||||
map(el => el.closest("details")!),
|
||||
filter(el => el && !el.open),
|
||||
|
||||
/* Open details and temporarily reset anchor */
|
||||
tap(el => {
|
||||
el.setAttribute("open", "")
|
||||
location.hash = ""
|
||||
}),
|
||||
|
||||
/* Defer anchor jump to next animation frame */
|
||||
observeOn(animationFrameScheduler),
|
||||
tap(() => {
|
||||
location.hash = hash
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
.subscribe()
|
||||
|
||||
/* Return details elements */
|
||||
return els$
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
* IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { Observable } from "rxjs"
|
||||
import { Observable, of } from "rxjs"
|
||||
import { map } from "rxjs/operators"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
@@ -42,3 +42,39 @@ export function not(
|
||||
map(active => !active)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache the last value emitted by an observable in session storage
|
||||
*
|
||||
* Note that the value must be serializable as `JSON`.
|
||||
*
|
||||
* @template T - Value type
|
||||
*
|
||||
* @param key - Cache key
|
||||
* @param factory - Observable factory
|
||||
*
|
||||
* @return Value observable
|
||||
*/
|
||||
export function cache<T>(
|
||||
key: string, factory: () => Observable<T>
|
||||
): Observable<T> {
|
||||
const data = sessionStorage.getItem(key)
|
||||
if (data) {
|
||||
return of(JSON.parse(data) as T)
|
||||
|
||||
/* Retrieve value from observable factory and write to storage */
|
||||
} else {
|
||||
const value$ = factory()
|
||||
value$
|
||||
.subscribe(value => {
|
||||
try {
|
||||
sessionStorage.setItem(key, JSON.stringify(value))
|
||||
} catch (err) {
|
||||
/* Just swallow */
|
||||
}
|
||||
})
|
||||
|
||||
/* Return value observable */
|
||||
return value$
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,3 +77,40 @@ export function truncate(value: string, n: number): string {
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
/**
|
||||
* Round a number for display with source facts
|
||||
*
|
||||
* This is a reverse engineered implementation of GitHub's weird rounding
|
||||
* algorithm for stars, forks and all other numbers. Probably incorrect.
|
||||
*
|
||||
* @param value - Original value
|
||||
*
|
||||
* @return Rounded value
|
||||
*/
|
||||
export function round(value: number): string {
|
||||
if (value > 999) {
|
||||
const digits = +((value - 950) % 1000 > 99)
|
||||
return `${((value + 1) / 1000).toFixed(digits)}k`
|
||||
} else {
|
||||
return value.toString()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple hash function
|
||||
*
|
||||
* @see https://bit.ly/2wsVjJ4 - Original source
|
||||
*
|
||||
* @param value - Value to be hashed
|
||||
*
|
||||
* @return Hash
|
||||
*/
|
||||
export function hash(value: string): number {
|
||||
let k = 0
|
||||
for (let i = 0, len = value.length; i < len; i++) {
|
||||
k = ((k << 5) - k) + value.charCodeAt(i)
|
||||
k |= 0 // Convert to 32bit integer
|
||||
}
|
||||
return k
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user