Filter = filters: {} results: {} init: -> return unless g.VIEW in ['index', 'thread'] and Conf['Filter'] unless Conf['Filtered Backlinks'] $.addClass doc, 'hide-backlinks' nsfwBoards = BoardConfig.sfwBoards(false).join(',') sfwBoards = BoardConfig.sfwBoards(true).join(',') for key of Config.filter for line in Conf[key].split '\n' continue if line[0] is '#' if not (regexp = line.match /\/(.*)\/(\w*)/) continue # Don't mix up filter flags with the regular expression. filter = line.replace regexp[0], '' # Comma-separated list of the boards this filter applies to. # Defaults to global. boardsRaw = filter.match(/boards:([^;]+)/)?[1].toLowerCase() if boardsRaw boards = {} for board in boardsRaw.replace('nsfw', nsfwBoards).replace('sfw', sfwBoards).split(',') boards[board] = true else boards = false # boards to exclude from an otherwise global rule # due to the sfw and nsfw keywords, also works on all filters # replaces 'nsfw' and 'sfw' for consistency excludesRaw = filter.match(/exclude:([^;]+)/)?[1].toLowerCase() if excludesRaw excludes = {} for board in excludesRaw.replace('nsfw', nsfwBoards).replace('sfw', sfwBoards).split(',') excludes[board] = true else excludes = false if (isstring = (key in ['uniqueID', 'MD5'])) # MD5 filter will use strings instead of regular expressions. regexp = regexp[1] else try # Please, don't write silly regular expressions. regexp = RegExp regexp[1], regexp[2] catch err # I warned you, bro. new Notice 'warning', [ $.tn "Invalid #{key} filter:" $.el 'br' $.tn line $.el 'br' $.tn err.message ], 60 continue # Filter OPs along with their threads or replies only. op = filter.match(/[^t]op:(no|only)/)?[1] or '' mask = {'no': 1, 'only': 2}[op] or 0 # Filter only posts with/without files. file = filter.match(/file:(no|only)/)?[1] or '' mask = mask | ({'no': 4, 'only': 8}[file] or 0) # Overrule the `Show Stubs` setting. # Defaults to stub showing. stub = switch filter.match(/stub:(yes|no)/)?[1] when 'yes' true when 'no' false else Conf['Stubs'] # Desktop notification noti = /notify/.test filter # Highlight the post. # If not specified, the highlight class will be filter-highlight. if (hl = /highlight/.test filter) hl = filter.match(/highlight:([\w-]+)/)?[1] or 'filter-highlight' # Put highlighted OP's thread on top of the board page or not. # Defaults to on top. top = filter.match(/top:(yes|no)/)?[1] or 'yes' top = top is 'yes' # Turn it into a boolean # Fields that this filter applies to (for 'general' filters) if key is 'general' if (types = filter.match /(?:^|;)\s*type:([^;]*)/) types = types[1].split(',').filter (x) -> x of Config.filter and x isnt 'general' else types = ['subject', 'name', 'filename', 'comment'] # Hide the post (default case). hide = !(hl or noti) filter = {isstring, regexp, boards, excludes, mask, hide, stub, hl, top, noti} if key is 'general' for type in types (@filters[type] or= []).push filter else (@filters[key] or= []).push filter return unless Object.keys(@filters).length Callbacks.Post.push name: 'Filter' cb: @node test: (post, hideable=true) -> return post.filterResults if post.filterResults hide = false stub = true hl = undefined top = false noti = false if QuoteYou.isYou(post) hideable = false mask = (if post.isReply then 2 else 1) mask = (mask | (if post.file then 4 else 8)) for key of Filter.filters when ((value = Filter.value key, post)?) # Continue if there's nothing to filter (no tripcode for example). for filter in Filter.filters[key] continue if ( (filter.boards and !filter.boards[post.boardID]) or (filter.excludes and filter.excludes[post.boardID]) or (filter.mask & mask) or (if filter.isstring then (filter.regexp isnt value) else !filter.regexp.test(value)) ) if filter.hide if hideable hide = true stub and= filter.stub else unless hl and filter.hl in hl (hl or= []).push filter.hl top or= filter.top if filter.noti noti = true if hide {hide, stub} else {hl, top, noti} node: -> return if @isClone {hide, stub, hl, top, noti} = Filter.test @, (!@isFetchedQuote and (@isReply or g.VIEW is 'index')) if hide if @isReply PostHiding.hide @, stub else ThreadHiding.hide @thread, stub else if hl @highlights = hl $.addClass @nodes.root, hl... if noti and Unread.posts and (@ID > Unread.lastReadPost) and not QuoteYou.isYou(@) Unread.openNotification @, ' triggered a notification filter' isHidden: (post) -> !!Filter.test(post).hide valueF: postID: (post) -> "#{post.ID}" name: (post) -> post.info.name uniqueID: (post) -> post.info.uniqueID or '' tripcode: (post) -> post.info.tripcode capcode: (post) -> post.info.capcode pass: (post) -> post.info.pass subject: (post) -> post.info.subject or (if post.isReply then undefined else '') comment: (post) -> (post.info.comment ?= Build.parseComment post.info.commentHTML.innerHTML) flag: (post) -> post.info.flag filename: (post) -> post.file?.name dimensions: (post) -> post.file?.dimensions filesize: (post) -> post.file?.size MD5: (post) -> post.file?.MD5 value: (key, post) -> Filter.valueF[key](post) addFilter: (type, re, cb) -> $.get type, Conf[type], (item) -> save = item[type] # Add a new line before the regexp unless the text is empty. save = if save "#{save}\n#{re}" else re $.set type, save, cb quickFilterMD5: -> post = Get.postFromNode @ return unless post.file Filter.addFilter 'MD5', "/#{post.file.MD5}/" origin = post.origin or post if origin.isReply PostHiding.hide origin else if g.VIEW is 'index' ThreadHiding.hide origin.thread # If post is still visible, give an indication that the MD5 was filtered. if post.nodes.post.getBoundingClientRect().height new Notice 'info', 'MD5 filtered.', 2 escape: (value) -> value.replace /// / | \\ | \^ | \$ | \n | \. | \( | \) | \{ | \} | \[ | \] | \? | \* | \+ | \| ///g, (c) -> if c is '\n' '\\n' else if c is '\\' '\\\\' else "\\#{c}" menu: init: -> return unless g.VIEW in ['index', 'thread'] and Conf['Menu'] and Conf['Filter'] div = $.el 'div', textContent: 'Filter' entry = el: div order: 50 open: (post) -> Filter.menu.post = post true subEntries: [] for type in [ ['Name', 'name'] ['Unique ID', 'uniqueID'] ['Tripcode', 'tripcode'] ['Capcode', 'capcode'] ['Pass Date', 'pass'] ['Subject', 'subject'] ['Comment', 'comment'] ['Flag', 'flag'] ['Filename', 'filename'] ['Image dimensions', 'dimensions'] ['Filesize', 'filesize'] ['Image MD5', 'MD5'] ] # Add a sub entry for each filter type. entry.subEntries.push Filter.menu.createSubEntry type[0], type[1] Menu.menu.addEntry entry createSubEntry: (text, type) -> el = $.el 'a', href: 'javascript:;' textContent: text el.dataset.type = type $.on el, 'click', Filter.menu.makeFilter return { el: el open: (post) -> value = Filter.value type, post value? } makeFilter: -> {type} = @dataset # Convert value -> regexp, unless type is MD5 value = Filter.value type, Filter.menu.post re = if type in ['uniqueID', 'MD5'] then value else Filter.escape(value) re = if type in ['uniqueID', 'MD5'] "/#{re}/" else "/^#{re}$/" Filter.addFilter type, re, -> # Open the settings and display & focus the relevant filter textarea. Settings.open 'Filter' section = $ '.section-container' select = $ 'select[name=filter]', section select.value = type Settings.selectFilter.call select $.onExists section, 'textarea', (ta) -> tl = ta.textLength ta.setSelectionRange tl, tl ta.focus()