README
social-components
Social service components.
Some experiments with common social service concepts such as a "post", its data structure, maybe some React components for rendering "post"s, etc.
GitHub Ban
On March 9th, 2020, GitHub, Inc. silently banned my account (erasing all my repos, issues and comments) without any notice or explanation. Because of that, all source codes had to be promptly moved to GitLab. The GitHub repo is now only used as a backup (you can star the repo there too), and the primary repo is now the GitLab one. Issues can be reported in any repo.
Install
npm install social-components --save
This library uses async
/await
syntax so including regenerator-runtime/runtime
is required when using it. In Node.js that usually means including @babel/runtime
. In a web browser that usually means including @babel/polyfill
(though starting from Babel 7.4.0
@babel/polyfill
has been deprecated in favor of manually including core-js/stable
and regenerator-runtime/runtime
).
Use
Currently this library is only used as a dependency of imageboard
and webapp-frontend
.
The <Post/>
React component is currently being experimented on.
Model
Services
- YouTube — Get video by URL or id.
- Vimeo — Get video by URL or id.
- Instagram — Get post by URL or id.
- Twitter — Get tweet by URL or id.
API
getPostText(post: Post, options: object?): string?
import getPostText from 'social-components/commonjs/utility/post/getPostText'
Returns a textual representation of a Post
Availble options
:
softLimit: number
— A "soft" limit on the resulting text length. "Soft" means that the resulting text may exceed the limit.messages: object?
— An object of shape{ contentType: { audio: 'Audio', ... } }
.skipPostQuoteBlocks: boolean?
— Skip all "block" (not "inline") post quotes.skipGeneratedPostQuoteBlocks: boolean?
— Skip all autogenerated "block" (not "inline") post quotes. Istrue
by default.skipAttachments: boolean?
— Skip attachments (embedded and non-embedded). Istrue
by default.skipNonEmbeddedAttachments: boolean?
— Skip non-embedded attachments. Istrue
by default.skipUntitledAttachments: boolean?
— Skip untitled attachments (embedded and non-embedded). Istrue
by default.trimCodeBlocksToFirstLine: boolean?
— Trim code blocks to first line. Istrue
by default.stopOnNewLine: boolean?
— Iftrue
then the function will stop on the first "new line" character of the generated text.
getInlineContentText(content: Content, options: object?): string?
import getInlineContentText from 'social-components/commonjs/utility/post/getInlineContentText'
Returns a textual representation of an InlineContent. Use this function as an equivalent of getPostText()
for inline-level content.
transformContent(content: Content, transform: function)
import transformContent from 'social-components/commonjs/utility/post/transformContent'
Recursively walks all parts of content
, calling transform(part)
for each such part. If transform(part)
returns:
undefined
, then thepart
is left unchanged, and it will recurse into thepart
's.content
.false
, then thepart
is left unchanged, and it won't recurse into thepart
's.content
.- an array, then the array is expanded in place of the
part
. - anything else, then the
part
is substituted with that.
generatePostQuote(post: Post, options: object?): string?
import generatePostQuote from 'social-components/commonjs/utility/post/generatePostQuote'
Generates a textual representation of a Post, that's intended to be used in a quote citing the post. Uses getPostText()
under the hood: first starts with a strict set of options, then gradually relaxes them until some text is generated. Then compacts inter-paragraph margins into just "new line" characters, and trims the resulting text to fit into options.maxLength
(adjusted by options.minFitFactor
and options.maxFitFactor
).
Availble options
:
- All
getPostText()
options are passed through. maxLength: number
— A limit on the resulting text length.minFitFactor: number?
— Provides some flexibility onmaxLength
. SeeminFitFactor
option oftrimText()
.minFitFactor: number?
— Provides some flexibility onmaxLength
. SeemaxFitFactor
option oftrimText()
.newLineCharacterLength: number?
— SeenewLineCharacterLength
option oftrimText()
.trimMarkEndOfLine: string?
— SeetrimMarkEndOfLine
option oftrimText()
.trimMarkEndOfSentence: string?
— SeetrimMarkEndOfSentence
option oftrimText()
.trimMarkEndOfWord: string?
— SeetrimMarkEndOfWord
option oftrimText()
.trimMarkAbrupt: string?
— SeetrimMarkAbrupt
option oftrimText()
.
generatePreview(post: Post, options: object): Content?
import generatePostPreview from 'social-components/commonjs/utility/post/generatePostPreview'
Generates a preview for a Post
given the options.limit
. If the post content
is undefined
then the returned preview content is too. Otherwise, the returned preview content isn't undefined
. If the post content
fits entirely then the preview content will be (deeply) equal to it. Otherwise, preview content will be a shortened version of content
with a { type: 'read-more' }
marker somewhere in the end.
Available options
:
maxLength: number
— Preview content (soft) limit (in "points": for text, one "point" is equal to one character, while any other non-text content has its own "points", including attachments and new line character).minFitFactor: number?
— Provides some flexibility onmaxLength
. Sets the usual lower limit of content trimming length atminFitFactor * maxLength
: if content surpassesmaxFitFactor * maxLength
limit, then it usually can be trimmed anywhere betweenminFitFactor * maxLength
andmaxFitFactor * maxLength
. Is1.2
by default.minFitFactor: number?
— Provides some flexibility onmaxLength
. Sets the usual upper limit of content trimming length atmaxFitFactor * maxLength
: if content surpassesmaxFitFactor * maxLength
limit, then it usually can be trimmed anywhere betweenminFitFactor * maxLength
andmaxFitFactor * maxLength
. Is0.75
by default.textTrimMarkEndOfWord: string?
— Appends this "trim mark" when text has to be trimmed after word end (but not after sentence end). Is "…" by default.textTrimMarkAbrupt: string?
— Appends this "trim mark" when text has to be trimmed mid-word. Is "…" by default.minimizeGeneratedPostLinkBlockQuotes: boolean?
— Set totrue
to indicate that post links with generated block quotes are initially minimized when rendered: this results in skipping counting those post links' content characters when generating post preview.
trimText(text: string, maxLength: number, options: object?): string
import trimText from 'social-components/commonjs/utility/post/trimText'
Trims the text
at maxLength
.
Available options
:
minFitFactor: number?
— Provides some flexibility onmaxLength
. Sets the lower limit of text trimming length atminFitFactor * maxLength
: if text length surpassesmaxFitFactor * maxLength
limit, then it can be trimmed anywhere betweenminFitFactor * maxLength
andmaxFitFactor * maxLength
. Is1
by default, meaning "no effect".minFitFactor: number?
— Provides some flexibility onmaxLength
. Sets the upper limit of text trimming length atmaxFitFactor * maxLength
: if text length surpassesmaxFitFactor * maxLength
limit, then it can be trimmed anywhere betweenminFitFactor * maxLength
andmaxFitFactor * maxLength
. Is1
by default, meaning "no effect".newLineCharacterLength: number?
— Can be set to specify custom character length for a "new line" (\n
) character. It could be used to "tax" multi-line texts when trimming. Is0
by default.trimPoint: string?
— Preferrable trim point. Can beundefined
(default),"sentence-end"
,"sentence-or-word-end"
.Preferrable trim point. By default it starts with seeing if it can trim at"sentence-end"
, then tries to trim at"sentence-or-word-end"
, and then just trims at any point.trimMarkEndOfLine: string?
— "Trim mark" when trimming at the end of a line. Is "" (no trim mark) by default.trimMarkEndOfSentence: string?
— "Trim mark" when trimming at the end of a sentence. Is "" (no trim mark) by default.trimMarkEndOfWord: string?
— "Trim mark" when trimming at the end of a word. Is " …" by default.trimMarkAbrupt: string?
— "Trim mark" when trimming mid-word. Is "…" by default.
censorWords(text: string, filters: WordFilter[]): Content
import censorWords from 'social-components/commonjs/utility/post/censorWords'
Replaces words in text
matching the filters
with objects of shape { type: "spoiler", censored: true, content: "the-word-that-got-censored" }
. The filters: WordFilter[]
argument must be a list of word filters pre-compiled with the exported compileWordPatterns(censoredWords, language)
function (described below).
const FILTERS = compileWordPatterns(['red', 'brown', 'orange'], 'en')
censorWords('A red fox', FILTERS) === [
'A ',
{ type: 'spoiler', censored: true, content: 'red' }
' fox'
]
compileWordPatterns(patterns: string[], language: string): WordFilter[]
import compileWordPatterns from 'social-components/commonjs/utility/post/compileWordPatterns'
Compiles word patterns into filters
that can be used in censorWords()
.
Arguments:
patterns: string[]
— An array ofstring
word patterns. The standard regular expression syntax applies,^
meaning "word start",$
meaning "word end",.
meaning "any letter", etc.language: string
— A lowercase two-letter language code (examples:"en"
,"ru"
,"de"
) that is used to generate a regular expression for splitting text into individual words.
Word pattern syntax:
^
— Word start.$
— Word end..
— Any letter.[abc]
— Any single one of letters: "a", "b", "c".a?
— Optional letter "a"..*
— Any count of any letter..+
— One or more of any letter..{0,2}
— Zero to two of any letter.
Word pattern examples:
^mother.*
— Matches"mothercare"
and"motherfather"
.^mother[f].*
— Matches"motherfather"
but not"mothercare"
.^mother[^f].*
— Matches"mothercare"
but not"motherfather"
.^cock$
— Matches"cock"
in"my cock is big"
but won't match"cocktail"
or"peacock"
.cock
— Matches"cock"
,"cocktail"
and"peacock"
.cock$
— Matches"cock"
and"peacock"
but not"cocktail"
.^cocks?
— Matches"cock"
and"cocks"
.^cock.{0,3}
— Matches"cock"
,"cocks"
,"cocker"
,"cockers"
.
loadResourceLinks(post: Post, options: object?): Promise
import loadResourceLinks from 'social-components/commonjs/utility/post/loadResourceLinks'
Loads "resource" links (such as links to YouTube and Twitter) by loading the info associated with the resources. For example, sets video .attachment
on YouTube links and sets "social" .attachment
on Twitter links.
Returns an object having a cancel()
function and a promise
property. Calling stop()
function prevents loadResourceLinks()
from making any further changes to the post
object. Calling cancel()
function multiple times or after the promise
has finished doesn't have any effect. The promise
is a Promise
that resolves when all resource links have been loaded. Loading some links may potentially error (for example, if a YouTube video wasn't found). Even if the returned Promise
errors due to some resource loader, the post content still may have been changed by other resource loaders.
Available options
:
onContentChange: function?
— Is called on this post content change (as a result of a resource link being loaded). For example, it may re-render the post.
getYouTubeVideoByUrl: function?
— Can be used for getting YouTube videos by URL from cache.youTubeApiKey: (string|string[])?
— YouTube API key. If it's an array of keys then the first non-erroring one is used.loadPost: function?
— This is a hacky point of customization to add some other custom resource loaders. Is used incaptchan
to fixlynxchan
post attachment sizes and URLs.contentMaxLength: number?
— If set, will re-generate.contentPreview
if the updatedcontent
exceeds the limit (in "points").messages: object?
— Localized labels ("Video", "Picture", etc). Has shape{ contentType: { video: "Video", ... }, post: { videoNotFound: "Video not found" } }
.minimizeGeneratedPostLinkBlockQuotes: boolean?
— See the description of the same option ofgeneratePostPreview()
.
expandStandaloneAttachmentLinks(content: Content)
Expands attachment links (objects of shape { type: 'link', attachment: ... }
into standalone attachments (block-level attachments: { type: 'attachment' }
).
import expandStandaloneAttachmentLinks from "social-components/commonjs/utility/post/expandStandaloneAttachmentLinks"
const content = [
[
"See ",
{
type: "link",
attachment: {
type: "video",
video: ...
}
},
" for more info."
]
]
expandStandaloneAttachmentLinks(content)
content === [
[
"See "
],
{
type: "attachment",
attachment: {
type: "video",
video: ...
}
},
[
" for more info."
]
]
visitPostParts(type: string, visit: function, content: Content): any[]
import visitPostParts from 'social-components/commonjs/utility/post/visitPostParts'
Calls visit(part)
on each part of content
being of type
type. Returns a list of results returned by each visit(part)
. For example, the following example prints URLs of all { type: 'link' }
s in a post content.
visitPostParts('link', link => console.log(link.url), post.content)
trimContent(content: Content, options: object?): Content?
import trimContent from 'social-components/commonjs/utility/post/trimContent'
Trims whitespace (including newlines) in the beginning and in the end of content
. content
internals will be mutated. Returns the mutated content
(the original content
still gets mutated). Returns undefined
if content
became empty as a result of the trimming.
Available options:
left: boolean
— Passleft: false
to prevent it from trimming on the left side.right: boolean
— Passright: false
to prevent it from trimming on the right side.
trimContent([['\n', ' Text '], ['\n']]) === [['Text']]
trimInlineContent(inlineContent: InlineContent, options: object?): InlineContent?
import trimInlineContent from 'social-components/commonjs/utility/post/trimInlineContent'
Trims whitespace (including newlines) in the beginning and in the end of content
. content
must be an array. content
internals will be mutated. Returns the mutated content
(the original content
still gets mutated). Returns undefined
if content
became empty as a result of the trimming.
Available options:
left: boolean
— Passleft: false
to prevent it from trimming on the left side.right: boolean
— Passright: false
to prevent it from trimming on the right side.
trimInlineContent(['\n', { type: 'text', content: ' Text ' }, '\n'])
=== [{ type: 'text', content: 'Text' }]
YouTube.getVideoByUrl(url: string, options: object): Promise<object>
import getVideoByUrl from 'social-components/commonjs/services/YouTube/getVideoByUrl'
Parses a YouTube video URL, queries the video info via YouTube V3 Data API and returns a Promise
resolving to { type: 'video' }
object.
Available options
:
youTubeApiKey: string
— YouTube V3 Data API key.
Some additional YouTube utilities:
// The list of possible YouTube preview picture ("thumbnail") sizes.
import { PREVIEW_PICTURE_SIZES } from 'social-components/commonjs/services/YouTube/getVideo'
PREVIEW_PICTURE_SIZES[0] === {
name: 'maxresdefault',
width: 1280,
height: 720
}
// Returns a URL of a YouTube video's preview picture ("thumbnail").
import { getPictureSizeUrl } from 'social-components/commonjs/services/YouTube/getVideo'
getPictureSizeUrl(videoId, size.name)
// Returns a YouTube video URL.
import getVideoUrl from 'social-components/commonjs/services/YouTube/getVideoUrl'
getVideoUrl(videoId, { startAt }?)
// Returns a URL for an embedded YouTube video.
// Can be used for embedding a video on a page via an `<iframe/>`.
import getEmbeddedVideoUrl from 'social-components/commonjs/services/YouTube/getEmbeddedVideoUrl'
getEmbeddedVideoUrl(videoId, { autoPlay, startAt }?)
Vimeo.getVideoByUrl(url: string): Promise<object>
import getVideoByUrl from 'social-components/commonjs/services/Vimeo/getVideoByUrl'
Parses a Vimeo video URL, queries the video info via HTTP REST API and returns a Promise
resolving to { type: 'video' }
object.
Some additional Vimeo utilities:
// Returns a Vimeo video URL.
import getVideoUrl from 'social-components/commonjs/services/Vimeo/getVideoUrl'
getVideoUrl(videoId)
// Returns a URL for an embedded Vimeo video.
// Can be used for embedding a video on a page via an `<iframe/>`.
import getEmbeddedVideoUrl from 'social-components/commonjs/services/Vimeo/getEmbeddedVideoUrl'
getEmbeddedVideoUrl(videoId, { color, autoPlay, loop }?)
Twitter.getTweetByUrl(url: string, options: object): Promise<object>
import getTweetByUrl from 'social-components/commonjs/services/Twitter/getTweetByUrl'
Parses a Vimeo video URL, queries the video info via HTTP REST API and returns a Promise
resolving to { type: 'social' }
object.
Available options
:
messages: object?
— Localized labels ("Link", "Attachment"). An object of shape{ link: "Link", attachment: "Attachment" }
.
Instagram.getPostByUrl(url: string): Promise<object>
import getPostByUrl from 'social-components/commonjs/services/Instagram/getPostByUrl'
Parses a Vimeo video URL, queries the video info via HTTP REST API and returns a Promise
resolving to { type: 'social' }
object.
Miscellaneous API
unescapeContent(string: string): string
import unescapeContent from 'social-components/commonjs/utility/unescapeContent'
Unescapes HTML-escaped text.
unescapeContent("<div/>") === "<div/>"
getColorHash(string: string): string
import getColorHash from 'social-components/commonjs/utility/getColorHash'
Converts a string to a color.
getColorHash("Some text") === "#aabbcc"
getHumanReadableLinkAddress(url: string): string
import getHumanReadableLinkAddress from 'social-components/commonjs/utility/getHumanReadableLinkAddress'
Returns a more human-friendly link address: strips "http(s):" protocol and the "www." part, removes a trailing slash.
getContentBlocks(post: Post): any[]
import getPostThumbnailAttachment from 'social-components/commonjs/utility/post/getContentBlocks'
Returns a list of "content blocks" of the post
's .content
. For example, if the post
's .content
is just a string, then it returns an array of that string. Returns an empty array if the post
has no content.
removeLeadingPostLink(post: Post, postLinkTest: (any|function))
Removes a leading post-link
, that satisfies the conditions, from the post
's .content
. Can be used to remove parent post quotes from replies when showing the parent post's replies tree.
The postLinkTest
argument should be the condition for removing a post link: either a post id or a function that returns true
when the post link should be removed.
import removeLeadingPostLink from 'social-components/commonjs/utility/post/removeLeadingPostLink'
comment.replies = comment.replies.map((reply) => {
return removeLeadingPostLink(reply, comment.id)
// Or, the same:
// return removeLeadingPostLink(reply, (postLink) => postLink.postId === comment.id)
})
getPostThumbnailAttachment(post: Post, options: object?): Attachment?
import getPostThumbnailAttachment from 'social-components/commonjs/utility/post/getPostThumbnailAttachment'
Returns an attachment that could be used as a "thumbnail" for this post. For example, could return a Picture
or a Video
.
Available options:
showPostThumbnailWhenThereAreMultipleAttachments
— Passtrue
to allow returning post thumbnail in cases when thepost
has multiple thumbnail-able attachments. By default, if thepost
has multiple thumbnail-able attachments, none of them will be returned.showPostThumbnailWhenThereIsNoContent
— Passtrue
to allow returning post thumbnail in cases when thepost
has nocontent
. By default, if thepost
has nocontent
, no post thumbnail will be returned.
getPicturesAndVideos(attachments: Attachment[]): Attachment[]
import getPicturesAndVideos from 'social-components/commonjs/utility/post/getPicturesAndVideos'
Returns Picture
s and Video
s.
getMinSize(picture: Picture): object
import getPicturesAndVideos from 'social-components/commonjs/utility/picture/getMinSize'
Returns the minimum "size" of a picture
, "size" being an object of shape: { width: number, height: number, url: string }
.
getNonEmbeddedAttachments(post: Post): Attachment[]
import getNonEmbeddedAttachments from 'social-components/commonjs/utility/post/getNonEmbeddedAttachments'
Returns a list of post attachments that aren't embedded in the post
's .content
.
getSortedAttachments(post: Post): Attachment[]?
import getSortedAttachments from 'social-components/commonjs/utility/post/getSortedAttachments'
Sorts post attachments in the order they appear embedded in the post
, plus all the rest of them that aren't embedded in the post
, sorted by thumbnail height descending.
Returns undefined
if the post
doesn't have any attachments.
getEmbeddedAttachment(block: ContentBlock, attachments: Attachment[]?): Attachment?
import getEmbeddedAttachment from 'social-components/commonjs/utility/post/getEmbeddedAttachment'
Returns an Attachment
object for an embedded attachment block.
hasPicture(attachment: Attachment): boolean?
import hasPicture from 'social-components/commonjs/utility/attachment/hasPicture'
Returns true
if the attachment
has a picture. Examples: Picture
, Video
.
getThumbnailSize(attachment: Attachment): object?
import getThumbnailSize from 'social-components/commonjs/utility/attachment/getThumbnailSize'
Returns the attachment
's thumbnail size. Returns undefined
if the attachment
doesn't have a thumbnail.
createLink(url: string, content: string?): object
import createLink from 'social-components/commonjs/utility/post/createLink'
Creates a link
object from a link's URL (url
) and a link's text (content
). The link
object can additionally have a service: string
property (example: "youtube"
).
The url
will be parsed to see if a service
can be detec For example, a YouTube video URL will be parsed and the resulting link
will have service
set to "youtube"
and content
set to the video ID, and a YouTube icon could then be shown before the link's content
when the link is rendered (or, if there's no icon for a service
, a simple ${service}:
prefix could be prepended to it).
getMimeType(url: string): string?
import getMimeType from 'social-components/commonjs/utility/getMimeType'
Returns a MIME type by file URL (or filesystem path, or filename). Basically, looks at the file extension. Returns undefined
if the MIME type couldn't be determined.
combineQuotes(content: Content)
import combineQuotes from 'social-components/commonjs/utility/post/combineQuotes'
Combines { type: "quote" }
objects on consequtive lines into a single { type: "quote" }
object with "\n"
s inside. Mutates the original content
.
findContentPart(content: Content, test: function, options: object?): number[]?
import findContentPart from 'social-components/commonjs/utility/post/findContentPart'
Recursively searches content
for the parts for which the test(part)
function returns true
. Returns an "index path" (an array of recursive content part indexes, like a path in a "tree").
The test
function should be a:
function(
part: (string|object),
{ getNextPart }
): boolean?
Available options:
backwards: boolean
— Passtrue
to search from the end to the start of thecontent
.
splitContent(content: Content, indexPath: number[], options: object?): Content[]
import splitContent from 'social-components/commonjs/utility/post/splitContent'
Splits content
by the indexPath
path into a leftPart: Content
and a rightPart: Content
. The indexPath
could be obtained from findContentPart()
.
renderTweet(tweetId: string, container: Element, options: object?)
import renderTweet from 'social-components/commonjs/Twitter/renderTweet'
Renders a tweet in a container
.
Available options:
darkMode: boolean
— Passtrue
to render the tweet in dark mode.locale: string
— Viewer's language code (examples:"en"
,"ru"
,"de"
).
To do
source/services/YouTube/getVideo.js
andsource/services/Vimeo/getVideo.js
both usefetch()
global function which isn't supported in Node.js. Developers using this package could optionally polyfillfetch()
on server side (for example, see/fetch-polyfill
).source/services/Twitter/getTweet.js
andsource/services/Instagram/getPost.js
both usefetch-jsonp
which doesn't work in Node.js. Some tests are skipped because of that (describe.skip()
). Maybe somehow substitutefetch-jsonp
for server side.