Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

prosemirror-transform

Package Overview
Dependencies
Maintainers
1
Versions
73
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

prosemirror-transform - npm Package Compare versions

Comparing version 1.2.4 to 1.2.5-beta1

2

package.json
{
"name": "prosemirror-transform",
"version": "1.2.4",
"version": "1.2.5-beta1",
"description": "ProseMirror document transformations",

@@ -5,0 +5,0 @@ "main": "dist/index.js",

@@ -18,15 +18,3 @@ import {Fragment, Slice} from "prosemirror-model"

if (fitsTrivially($from, $to, slice)) return new ReplaceStep(from, to, slice)
let placed = placeSlice($from, slice)
let fittedLeft = fitLeft($from, placed)
let fitted = fitRight($from, $to, fittedLeft)
if (!fitted) return null
if (fittedLeft.size != fitted.size && canMoveText($from, $to, fittedLeft)) {
let d = $to.depth, after = $to.after(d)
while (d > 1 && after == $to.end(--d)) ++after
let fittedAfter = fitRight($from, doc.resolve(after), fittedLeft)
if (fittedAfter)
return new ReplaceAroundStep(from, after, to, $to.end(), fittedAfter, fittedLeft.size)
}
return fitted.size || from != to ? new ReplaceStep(from, to, fitted) : null
return new Fitter($from, $to, slice).fit()
}

@@ -62,122 +50,2 @@

function fitLeftInner($from, depth, placed, placedBelow) {
let content = Fragment.empty, openEnd = 0, placedHere = placed[depth]
if ($from.depth > depth) {
let inner = fitLeftInner($from, depth + 1, placed, placedBelow || placedHere)
openEnd = inner.openEnd + 1
content = Fragment.from($from.node(depth + 1).copy(inner.content))
}
if (placedHere) {
content = content.append(placedHere.content)
openEnd = placedHere.openEnd
}
if (placedBelow) {
content = content.append($from.node(depth).contentMatchAt($from.indexAfter(depth)).fillBefore(Fragment.empty, true))
openEnd = 0
}
return {content, openEnd}
}
function fitLeft($from, placed) {
let {content, openEnd} = fitLeftInner($from, 0, placed, false)
return new Slice(content, $from.depth, openEnd || 0)
}
function fitRightJoin(content, parent, $from, $to, depth, openStart, openEnd) {
let match, count = content.childCount, matchCount = count - (openEnd > 0 ? 1 : 0)
let parentNode = openStart < 0 ? parent : $from.node(depth)
if (openStart < 0)
match = parentNode.contentMatchAt(matchCount)
else if (count == 1 && openEnd > 0)
match = parentNode.contentMatchAt(openStart ? $from.index(depth) : $from.indexAfter(depth))
else
match = parentNode.contentMatchAt($from.indexAfter(depth))
.matchFragment(content, count > 0 && openStart ? 1 : 0, matchCount)
let toNode = $to.node(depth)
if (openEnd > 0 && depth < $to.depth) {
let after = toNode.content.cutByIndex($to.indexAfter(depth)).addToStart(content.lastChild)
let joinable = match.fillBefore(after, true)
// Can't insert content if there's a single node stretched across this gap
if (joinable && joinable.size && openStart > 0 && count == 1) joinable = null
if (joinable) {
let inner = fitRightJoin(content.lastChild.content, content.lastChild, $from, $to,
depth + 1, count == 1 ? openStart - 1 : -1, openEnd - 1)
if (inner) {
let last = content.lastChild.copy(inner)
if (joinable.size)
return content.cutByIndex(0, count - 1).append(joinable).addToEnd(last)
else
return content.replaceChild(count - 1, last)
}
}
}
if (openEnd > 0)
match = match.matchType((count == 1 && openStart > 0 ? $from.node(depth + 1) : content.lastChild).type)
// If we're here, the next level can't be joined, so we see what
// happens if we leave it open.
let toIndex = $to.index(depth)
if (toIndex == toNode.childCount && !toNode.type.compatibleContent(parent.type)) return null
let joinable = match.fillBefore(toNode.content, true, toIndex)
for (let i = toIndex; joinable && i < toNode.content.childCount; i++)
if (!parentNode.type.allowsMarks(toNode.content.child(i).marks)) joinable = null
if (!joinable) return null
if (openEnd > 0) {
let closed = fitRightClosed(content.lastChild, openEnd - 1, $from, depth + 1,
count == 1 ? openStart - 1 : -1)
content = content.replaceChild(count - 1, closed)
}
content = content.append(joinable)
if ($to.depth > depth)
content = content.addToEnd(fitRightSeparate($to, depth + 1))
return content
}
function fitRightClosed(node, openEnd, $from, depth, openStart) {
let match, content = node.content, count = content.childCount
if (openStart >= 0)
match = $from.node(depth).contentMatchAt($from.indexAfter(depth))
.matchFragment(content, openStart > 0 ? 1 : 0, count)
else
match = node.contentMatchAt(count)
if (openEnd > 0) {
let closed = fitRightClosed(content.lastChild, openEnd - 1, $from, depth + 1,
count == 1 ? openStart - 1 : -1)
content = content.replaceChild(count - 1, closed)
}
return node.copy(content.append(match.fillBefore(Fragment.empty, true)))
}
function fitRightSeparate($to, depth) {
let node = $to.node(depth)
let fill = node.contentMatchAt(0).fillBefore(node.content, true, $to.index(depth))
if ($to.depth > depth) fill = fill.addToEnd(fitRightSeparate($to, depth + 1))
return node.copy(fill)
}
function normalizeSlice(content, openStart, openEnd) {
while (openStart > 0 && openEnd > 0 && content.childCount == 1) {
content = content.firstChild.content
openStart--
openEnd--
}
return new Slice(content, openStart, openEnd)
}
// : (ResolvedPos, ResolvedPos, number, Slice) → Slice
function fitRight($from, $to, slice) {
let fitted = fitRightJoin(slice.content, $from.node(0), $from, $to, 0, slice.openStart, slice.openEnd)
if (!fitted) return null
return normalizeSlice(fitted, slice.openStart, $to.depth)
}
function fitsTrivially($from, $to, slice) {

@@ -188,26 +56,2 @@ return !slice.openStart && !slice.openEnd && $from.start() == $to.start() &&

function canMoveText($from, $to, slice) {
if (!$to.parent.isTextblock) return false
let parent = slice.openEnd ? nodeRight(slice.content, slice.openEnd)
: $from.node($from.depth - (slice.openStart - slice.openEnd))
if (!parent.isTextblock) return false
for (let i = $to.index(); i < $to.parent.childCount; i++)
if (!parent.type.allowsMarks($to.parent.child(i).marks)) return false
let match
if (slice.openEnd) {
match = parent.contentMatchAt(parent.childCount)
} else {
match = parent.contentMatchAt(parent.childCount)
if (slice.size) match = match.matchFragment(slice.content, slice.openStart ? 1 : 0)
}
match = match.matchFragment($to.parent.content, $to.index())
return match && match.validEnd
}
function nodeRight(content, depth) {
for (let i = 1; i < depth; i++) content = content.lastChild.content
return content.lastChild
}
// Algorithm for 'placing' the elements of a slice into a gap:

@@ -220,182 +64,274 @@ //

//
// So placeSlice splits up a slice into a number of sub-slices,
// along with information on where they can be placed on the given
// left-side edge. It works by walking the open side of the slice,
// from the inside out, and trying to find a landing spot for each
// element, by simultaneously scanning over the gap side. When no
// place is found for an open node's content, it is left in that node.
// This class tracks the state of the placement progress in the
// following properties:
//
// - `frontier` holds a stack of `{type, match}` objects that
// represent the open side of the replacement. It starts at
// `$from`, then moves forward as content is placed, and is finally
// reconciled with `$to`.
//
// - `unplaced` is a slice that represents the content that hasn't
// been placed yet.
//
// - `placed` is a fragment of placed content. Its open-start value
// is implicit in `$from`, and its open-end value in `frontier`.
class Fitter {
constructor($from, $to, slice) {
this.$to = $to
this.$from = $from
this.unplaced = slice
// : (ResolvedPos, Slice) → [{content: Fragment, openEnd: number, depth: number}]
function placeSlice($from, slice) {
let frontier = new Frontier($from)
for (let pass = 1; slice.size && pass <= 3; pass++) {
let value = frontier.placeSlice(slice.content, slice.openStart, slice.openEnd, pass)
if (pass == 3 && value != slice && value.size) pass = 0 // Restart if the 3rd pass made progress but left content
slice = value
this.frontier = []
for (let i = 0; i <= $from.depth; i++) {
let node = $from.node(i)
this.frontier.push({
type: node.type,
match: node.contentMatchAt($from.indexAfter(i))
})
}
this.placed = Fragment.empty
for (let i = $from.depth; i > 0; i--)
this.placed = Fragment.from($from.node(i).copy(this.placed))
}
while (frontier.open.length) frontier.closeNode()
return frontier.placed
}
// Helper class that models the open side of the insert position,
// keeping track of the content match and already inserted content
// at each depth.
class Frontier {
constructor($pos) {
// : [{parent: Node, match: ContentMatch, content: Fragment, wrapper: bool, openEnd: number, depth: number}]
this.open = []
for (let d = 0; d <= $pos.depth; d++) {
let parent = $pos.node(d), match = parent.contentMatchAt($pos.indexAfter(d))
this.open.push({parent, match, content: Fragment.empty, wrapper: false, openEnd: 0, depth: d})
get depth() { return this.frontier.length - 1 }
fit() {
// As long as there's unplaced content, try to place some of it.
// If that fails, either increase the open score of the unplaced
// slice, or drop nodes from it, and then try again.
while (this.unplaced.size) {
let fit = this.findFittable()
if (fit) this.placeNodes(fit)
else this.openMore() || this.dropNode()
}
this.placed = []
// When there's inline content directly after the frontier _and_
// directly after `this.$to`, we must generate a `ReplaceAround`
// step that pulls that content into the node after the frontier.
// That means the fitting must be done to the end of the textblock
// node after `this.$to`, not `this.$to` itself.
let moveInline = this.mustMoveInline(), placedSize = this.placed.size - this.depth - this.$from.depth
let $from = this.$from, $to = moveInline < 0 ? this.$to : $from.doc.resolve(moveInline)
if (this.close($to)) { // If closing to `$to` succeeded, create a step
let content = this.placed, openStart = $from.depth, openEnd = $to.depth
while (openStart && openEnd && content.childCount == 1) { // Normalize by dropping open parent nodes
content = content.firstChild.content
openStart--; openEnd--
}
let slice = new Slice(content, openStart, openEnd)
if (moveInline > -1)
return new ReplaceAroundStep($from.pos, moveInline, this.$to.pos, this.$to.end(), slice, placedSize)
if (slice.size || $from.pos != this.$to.pos) // Don't generate no-op steps
return new ReplaceStep($from.pos, $to.pos, slice)
}
return null
}
// : (Fragment, number, number, number, ?Node) → Slice
// Tries to place the content of the given slice, and returns a
// slice containing unplaced content.
//
// pass 1: try to fit directly
// pass 2: allow wrapper nodes to be introduced
// pass 3: allow unwrapping of nodes that aren't open
placeSlice(fragment, openStart, openEnd, pass, parent) {
if (openStart > 0) {
let first = fragment.firstChild
let inner = this.placeSlice(first.content, Math.max(0, openStart - 1),
openEnd && fragment.childCount == 1 ? openEnd - 1 : 0,
pass, first)
if (inner.content != first.content) {
if (inner.content.size) {
fragment = fragment.replaceChild(0, first.copy(inner.content))
openStart = inner.openStart + 1
// Find a position on the start spine of `this.unplaced` that has
// content that can be moved somewhere on the frontier. Returns two
// depths, one for the slice and one for the frontier.
findFittable() {
// Only try wrapping nodes (pass 2) after finding a place without
// wrapping failed.
for (let pass = 1; pass <= 2; pass++) {
for (let sliceDepth = this.unplaced.openStart; sliceDepth >= 0; sliceDepth--) {
let fragment, parent
if (sliceDepth) {
parent = contentAt(this.unplaced.content, sliceDepth - 1).firstChild
fragment = parent.content
} else {
if (fragment.childCount == 1) openEnd = 0
fragment = fragment.cutByIndex(1)
openStart = 0
fragment = this.unplaced.content
}
let first = fragment.firstChild
for (let frontierDepth = this.depth; frontierDepth >= 0; frontierDepth--) {
let {type, match} = this.frontier[frontierDepth], wrap, inject
// In pass 1, if the next node matches, or there is no next
// node but the parents look compatible, we've found a
// place.
if (pass == 1 && (first ? match.matchType(first.type) || (inject = match.fillBefore(Fragment.from(first), false))
: type.compatibleContent(parent.type)))
return {sliceDepth, frontierDepth, parent, inject}
// In pass 2, look for a set of wrapping nodes that make
// `first` fit here.
else if (pass == 2 && first && (wrap = match.findWrapping(first.type)))
return {sliceDepth, frontierDepth, parent, wrap}
// Don't continue looking further up if the parent node
// would fit here.
if (parent && match.matchType(parent.type)) break
}
}
}
let result = this.placeContent(fragment, openStart, openEnd, pass, parent)
if (pass > 2 && result.size && openStart == 0) {
let child = result.content.firstChild, single = result.content.childCount == 1
this.placeContent(child.content, 0, openEnd && single ? openEnd - 1 : 0, pass, child)
result = single ? Fragment.empty : new Slice(result.content.cutByIndex(1), 0, openEnd)
}
return result
}
placeContent(fragment, openStart, openEnd, pass, parent) {
let i = 0
// Go over the fragment's children
for (; i < fragment.childCount; i++) {
let child = fragment.child(i), placed = false, last = i == fragment.childCount - 1
// Try each open node in turn, starting from the innermost
for (let d = this.open.length - 1; d >= 0; d--) {
let open = this.open[d], wrap
openMore() {
let {content, openStart, openEnd} = this.unplaced
let inner = contentAt(content, openStart)
if (!inner.childCount || inner.firstChild.isLeaf) return false
this.unplaced = new Slice(content, openStart + 1,
Math.max(openEnd, inner.size + openStart >= content.size - openEnd ? openStart + 1 : 0))
return true
}
// If pass > 1, it is allowed to wrap the node to help find a
// fit, so if findWrapping returns something, we add open
// nodes to the frontier for that wrapping.
if (pass > 1 && (wrap = open.match.findWrapping(child.type)) &&
!(parent && wrap.length && wrap[wrap.length - 1] == parent.type)) {
while (this.open.length - 1 > d) this.closeNode()
for (let w = 0; w < wrap.length; w++) {
open.match = open.match.matchType(wrap[w])
d++
open = {parent: wrap[w].create(),
match: wrap[w].contentMatch,
content: Fragment.empty, wrapper: true, openEnd: 0, depth: d + w}
this.open.push(open)
}
}
dropNode() {
let {content, openStart, openEnd} = this.unplaced
let inner = contentAt(content, openStart)
if (inner.childCount <= 1 && openStart > 0) {
let openAtEnd = content.size - openStart <= openStart + inner.size
this.unplaced = new Slice(dropFromFragment(content, openStart - 1, 1), openStart - 1,
openAtEnd ? openStart - 1 : openEnd)
} else {
this.unplaced = new Slice(dropFromFragment(content, openStart, 1), openStart, openEnd)
}
}
// See if the child fits here
let match = open.match.matchType(child.type)
if (!match) {
let fill = open.match.fillBefore(Fragment.from(child))
if (fill) {
for (let j = 0; j < fill.childCount; j++) {
let ch = fill.child(j)
this.addNode(open, ch, 0)
match = open.match.matchFragment(ch)
}
} else if (parent && open.match.matchType(parent.type)) {
// Don't continue looking further up if the parent node
// would fit here.
break
} else {
continue
}
}
// : ({sliceDepth: number, frontierDepth: number, parent: ?Node, wrap: ?[NodeType], inject: ?Fragment})
// Move content from the unplaced slice at `sliceDepth` to the
// frontier node at `frontierDepth`. Close that frontier node when
// applicable.
placeNodes({sliceDepth, frontierDepth, parent, inject, wrap}) {
while (this.depth > frontierDepth) this.closeFrontierNode()
if (wrap) for (let i = 0; i < wrap.length; i++) this.openFrontierNode(wrap[i])
// Close open nodes above this one, since we're starting to
// add to this.
while (this.open.length - 1 > d) this.closeNode()
// Strip marks from the child or close its start when necessary
child = child.mark(open.parent.type.allowedMarks(child.marks))
if (openStart) {
child = closeNodeStart(child, openStart, last ? openEnd : 0)
openStart = 0
}
// Add the child to this open node and adjust its metadata
this.addNode(open, child, last ? openEnd : 0)
open.match = match
if (last) openEnd = 0
placed = true
break
let slice = this.unplaced, fragment = parent ? parent.content : slice.content
let openStart = slice.openStart - sliceDepth
let taken = 0, add = []
let {match, type} = this.frontier[frontierDepth]
if (inject) {
for (let i = 0; i < inject.childCount; i++) add.push(inject.child(i))
match = match.matchFragment(inject)
}
// Computes the amount of (end) open nodes at the end of the
// fragment. When 0, the parent is open, but no more. When
// negative, nothing is open.
let openEndCount = (fragment.size + sliceDepth) - (slice.content.size - slice.openEnd)
// Scan over the fragment, fitting as many child nodes as
// possible.
while (taken < fragment.childCount) {
let next = fragment.child(taken), matches = match.matchType(next.type)
if (!matches) break
taken++
if (taken > 1 || openStart == 0 || next.content.size) { // Drop empty open nodes
match = matches
add.push(closeNodeStart(next.mark(type.allowedMarks(next.marks)), taken == 1 ? openStart : 0,
taken == fragment.childCount ? openEndCount : -1))
}
// As soon as we've failed to place a node we stop looking at
// later nodes
if (!placed) break
}
// Close the current open node if it's not the the root and we
// either placed up to the end of the node or the the current
// slice depth's node type matches the open node's type
if (this.open.length > 1 &&
(i > 0 && i == fragment.childCount ||
parent && this.open[this.open.length - 1].parent.type == parent.type))
this.closeNode()
let toEnd = taken == fragment.childCount
if (!toEnd) openEndCount = -1
return new Slice(fragment.cutByIndex(i), openStart, openEnd)
// If the parent types match, and the entire node was moved, and
// it's not open, close this frontier node right away.
if (toEnd && openEndCount < 0 && parent && parent.type == this.frontier[this.depth].type) this.closeFrontierNode()
else this.frontier[frontierDepth].match = match
// Add new frontier nodes for any open nodes at the end.
for (let i = 0, cur = fragment; i < openEndCount; i++) {
let node = cur.lastChild
this.frontier.push({type: node.type, match: node.contentMatchAt(node.childCount)})
cur = node.content
}
// Update `this.unplaced`. Drop the entire node from which we
// placed it we got to its end, otherwise just drop the placed
// nodes.
this.unplaced = !toEnd ? new Slice(dropFromFragment(slice.content, sliceDepth, taken), slice.openStart, slice.openEnd)
: sliceDepth == 0 ? Slice.empty
: new Slice(dropFromFragment(slice.content, sliceDepth - 1, 1),
sliceDepth - 1, openEndCount < 0 ? slice.openEnd : sliceDepth - 1)
this.placed = addToFragment(this.placed, frontierDepth, Fragment.from(add))
}
addNode(open, node, openEnd) {
open.content = closeFragmentEnd(open.content, open.openEnd).addToEnd(node)
open.openEnd = openEnd
mustMoveInline() {
if (!this.$to.parent.isTextblock) return -1
let top = this.frontier[this.depth], level
if (!top.type.isTextblock || !contentAfterFits(this.$to, this.$to.depth, top.type, top.match, false) ||
(this.$to.depth == this.depth && (level = this.findCloseLevel(this.$to)) && level.depth == this.depth)) return -1
let {depth} = this.$to, after = this.$to.after(depth)
while (depth > 1 && after == this.$to.end(--depth)) ++after
return after
}
closeNode() {
let open = this.open.pop()
if (open.content.size == 0) {
// Nothing here
} else if (open.wrapper) {
this.addNode(this.open[this.open.length - 1], open.parent.copy(open.content), open.openEnd + 1)
} else {
this.placed[open.depth] = {depth: open.depth, content: open.content, openEnd: open.openEnd}
findCloseLevel($to) {
scan: for (let i = Math.min(this.depth, $to.depth); i >= 0; i--) {
let {match, type} = this.frontier[i]
let fit = contentAfterFits($to, i, type, match, false)
if (!fit) continue
for (let d = i - 1; d >= 0; d--) {
let {match, type} = this.frontier[d]
let matches = contentAfterFits($to, d, type, match, true)
if (!matches || matches.childCount) continue scan
}
return {depth: i, fit}
}
}
close($to) {
let close = this.findCloseLevel($to)
if (!close) return false
while (this.depth > close.depth) this.closeFrontierNode()
if (close.fit.childCount) this.placed = addToFragment(this.placed, close.depth, close.fit)
for (let d = close.depth + 1; d <= $to.depth; d++) {
let node = $to.node(d), add = node.type.contentMatch.fillBefore(node.content, true, $to.index(d))
this.openFrontierNode(node.type, add)
}
return true
}
openFrontierNode(type, content) {
let top = this.frontier[this.depth]
if (top.type == type && type.name == "list_item") throw new Error("NOOOP")
top.match = top.match.matchType(type)
this.placed = addToFragment(this.placed, this.depth, Fragment.from(type.create(null, content)))
this.frontier.push({type, match: type.contentMatch})
}
closeFrontierNode() {
let open = this.frontier.pop()
let add = open.match.fillBefore(Fragment.empty, true)
if (add.childCount) this.placed = addToFragment(this.placed, this.frontier.length, add)
}
}
function dropFromFragment(fragment, depth, count) {
if (depth == 0) return fragment.cutByIndex(count)
return fragment.replaceChild(0, fragment.firstChild.copy(dropFromFragment(fragment.firstChild.content, depth - 1, count)))
}
function addToFragment(fragment, depth, content) {
if (depth == 0) return fragment.append(content)
return fragment.replaceChild(fragment.childCount - 1,
fragment.lastChild.copy(addToFragment(fragment.lastChild.content, depth - 1, content)))
}
function contentAt(fragment, depth) {
for (let i = 0; i < depth; i++) fragment = fragment.firstChild.content
return fragment
}
function closeNodeStart(node, openStart, openEnd) {
let content = node.content
if (openStart > 1) {
let first = closeNodeStart(node.firstChild, openStart - 1, node.childCount == 1 ? openEnd - 1 : 0)
content = node.content.replaceChild(0, first)
if (openStart <= 0) return node
let frag = node.content
if (openStart > 1)
frag = frag.replaceChild(0, closeNodeStart(frag.firstChild, openStart - 1, frag.childCount == 1 ? openEnd - 1 : 0))
if (openStart > 0) {
frag = node.type.contentMatch.fillBefore(frag).append(frag)
if (openEnd <= 0) frag = frag.append(node.type.contentMatch.matchFragment(frag).fillBefore(Fragment.empty, true))
}
let fill = node.type.contentMatch.fillBefore(content, openEnd == 0)
return node.copy(fill.append(content))
return node.copy(frag)
}
function closeNodeEnd(node, depth) {
let content = node.content
if (depth > 1) {
let last = closeNodeEnd(node.lastChild, depth - 1)
content = node.content.replaceChild(node.childCount - 1, last)
}
let fill = node.contentMatchAt(node.childCount).fillBefore(Fragment.empty, true)
return node.copy(content.append(fill))
function contentAfterFits($to, depth, type, match, open) {
let node = $to.node(depth), index = open ? $to.indexAfter(depth) : $to.index(depth)
if (index == node.childCount && !type.compatibleContent(node.type)) return null
let fit = match.fillBefore(node.content, true, index)
return fit && !invalidMarks(type, node.content, index) ? fit : null
}
function closeFragmentEnd(fragment, depth) {
return depth ? fragment.replaceChild(fragment.childCount - 1, closeNodeEnd(fragment.lastChild, depth)) : fragment
function invalidMarks(type, fragment, start) {
for (let i = start; i < fragment.childCount; i++)
if (!type.allowsMarks(fragment.child(i).marks)) return true
return false
}

@@ -555,1 +491,2 @@

}

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc