@ElMiko: This pattern should find all instances of the bullet character, excluding only the ones that you do not want.
Code:
(<p[^>]*?>)?\K•(?(1)(*SKIP)(?!</p>))
Let me break this pattern down:
(<p[^>]*?>)? : Match 1 or 0(also called optional match) p element opening tag, capture it if it exists.
\K : Resets the start of match (anything matched before this will not be included in the match result).
• : The match we are interested in, in our case it is the bullet character alone •.
(?(1) : If we have successfully found an opening p tag in the first capture group process the following, otherwise ignore them altogether:
(*SKIP) : advance the starting point in the next search iteration to here,
(?!</p>) : if we find a closing of the p element at this point(also fails the match).
)
Some more (wordy) explanations of the parts:
(?(1)yes-pattern) : This is a form of conditional subpattern, it searches for the yes-pattern if the condition is true, otherwise it has no effect. The condition in this form is the check for match for the first capture group in our complete pattern. In our case it is the conditional match at the beginning of the pattern. Read as "If the first capture group has matched anything then look for yes-pattern here, if the first capture group is empty(remember that we have used an optional match, so no-match was acceptable to our pattern) then ignore this whole subpattern."
yes-pattern in our example is (*SKIP)(?!</p>)
(*SKIP) : Normally when a match fails at any point, the starting point in the source that will be next tried for the pattern is advanced one character. (*SKIP) backtracking control verb causes the point to be advanced to where (*SKIP) verb is encountered during the matching if the pattern in whole fails matching. This implies two things are needed for the (*SKIP) verb to have an effect; 1) everything before it has already matched successfully, 2) something in the part of the pattern that comes after it caused a fail in match.
(?!</p>) : This is a classic negative lookahead pattern. If the subpattern is found at this point in the search, this causes a fail in the pattern. In our case it looks for </p>, and if this pattern is found here, (?!</p>) causes a fail in match, which in turn causes the (*SKIP) to have its effect and the starting point for the next search is advanced to the (*SKIP) point, which is the character just after the bullet, •.
Here is a templated form(whitespaces in here are for ease of reading):
(negative_lookbehind_pattern_simulator
)? \Kwanted_pattern
(?(1) (*SKIP) (?!negative_lookahed_pattern
))
Note that unlike regular lookbehind patterns, this form allows for indefinite length matches, because technically it is not a lookbehind assertion but a basic match pattern that starts searching from the current character position; it behaves like a negative lookbehind by the help of the later conditional part of the template. This is the reason why I have labelled this pattern as a "simulator".
Corollary:
Here is a modified form of this template one can use if only a negative lookbehind alternative is needed, which allows for indefinite length matches.
(negative_lookbehind_pattern_simulator
)? \Kwanted_pattern
(?(1) (*SKIP) (?!))
Here we use (?!) negative lookahead with an empty string as the subpattern. Empty string pattern always matches, hence its negative assertion always fails to match, hence (*SKIP) effect is achieved regardless of what comes after it as long as the first capture group is not empty.
The reason for the existence of the (*SKIP) in the pattern is left as an exercise to the reader.
The reason for why I am leaving it as an exercise is a sudden lazy spell.